ceci 2.2__tar.gz → 2.3__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.2 → ceci-2.3}/.gitignore +0 -1
- {ceci-2.2 → ceci-2.3}/PKG-INFO +7 -7
- ceci-2.3/ceci/__init__.py +18 -0
- ceci-2.3/ceci/_version.py +34 -0
- {ceci-2.2 → ceci-2.3}/ceci/main.py +13 -51
- {ceci-2.2 → ceci-2.3}/ceci/minirunner.py +1 -1
- ceci-2.3/ceci/pipeline/__init__.py +15 -0
- ceci-2.3/ceci/pipeline/dry_run.py +43 -0
- ceci-2.3/ceci/pipeline/file_manager.py +73 -0
- ceci-2.3/ceci/pipeline/flow_chart.py +8 -0
- ceci-2.3/ceci/pipeline/graph.py +199 -0
- ceci-2.3/ceci/pipeline/mini.py +133 -0
- ceci-2.3/ceci/pipeline/parsl.py +169 -0
- ceci-2.3/ceci/pipeline/pipeline.py +954 -0
- ceci-2.3/ceci/pipeline/sec.py +173 -0
- ceci-2.3/ceci/pipeline/templates.py +54 -0
- {ceci-2.2 → ceci-2.3}/ceci/sites/__init__.py +1 -1
- {ceci-2.2 → ceci-2.3}/ceci/sites/ccin2p3.py +0 -3
- {ceci-2.2 → ceci-2.3}/ceci/sites/local.py +0 -3
- {ceci-2.2 → ceci-2.3}/ceci/sites/nersc.py +0 -3
- {ceci-2.2 → ceci-2.3}/ceci/sites/site.py +0 -1
- {ceci-2.2 → ceci-2.3}/ceci/stage.py +1 -144
- ceci-2.3/ceci/tools/ancestors.py +31 -0
- {ceci-2.2 → ceci-2.3}/ceci.egg-info/PKG-INFO +7 -7
- {ceci-2.2 → ceci-2.3}/ceci.egg-info/SOURCES.txt +13 -1
- {ceci-2.2 → ceci-2.3}/ceci.egg-info/requires.txt +4 -6
- {ceci-2.2 → ceci-2.3}/docs/config.rst +3 -3
- {ceci-2.2 → ceci-2.3}/docs/launchers.rst +1 -23
- {ceci-2.2 → ceci-2.3}/docs/stages.rst +1 -1
- ceci-2.3/nb/ceci_interactive_example.ipynb +567 -0
- {ceci-2.2 → ceci-2.3}/pyproject.toml +5 -6
- ceci-2.3/tests/north.yml +9 -0
- ceci-2.3/tests/template.yml +93 -0
- {ceci-2.2 → ceci-2.3}/tests/test.yml +0 -5
- {ceci-2.2 → ceci-2.3}/tests/test_dataregistry.yml +0 -5
- {ceci-2.2 → ceci-2.3}/tests/test_executables.py +12 -1
- ceci-2.3/tests/test_graph.py +99 -0
- ceci-2.3/tests/test_main.py +190 -0
- {ceci-2.2 → ceci-2.3}/tests/test_namespace.yml +0 -5
- {ceci-2.2 → ceci-2.3}/tests/test_pipeline.py +85 -19
- {ceci-2.2 → ceci-2.3}/tests/test_snapshot.py +6 -0
- ceci-2.2/ceci/__init__.py +0 -19
- ceci-2.2/ceci/_version.py +0 -16
- ceci-2.2/ceci/pipeline.py +0 -1744
- ceci-2.2/ceci/tools/ancestors.py +0 -50
- ceci-2.2/nb/ceci_interactive_example.ipynb +0 -314
- ceci-2.2/tests/test_main.py +0 -103
- {ceci-2.2 → ceci-2.3}/.coveragerc +0 -0
- {ceci-2.2 → ceci-2.3}/.github/workflows/ci.yml +0 -0
- {ceci-2.2 → ceci-2.3}/.github/workflows/publish.yml +0 -0
- {ceci-2.2 → ceci-2.3}/.pylintrc +0 -0
- {ceci-2.2 → ceci-2.3}/LICENSE +0 -0
- {ceci-2.2 → ceci-2.3}/README.md +0 -0
- {ceci-2.2 → ceci-2.3}/ceci/__main__.py +0 -0
- {ceci-2.2 → ceci-2.3}/ceci/config.py +0 -0
- {ceci-2.2 → ceci-2.3}/ceci/errors.py +0 -0
- {ceci-2.2 → ceci-2.3}/ceci/monitor.py +0 -0
- {ceci-2.2 → ceci-2.3}/ceci/tools/__init__.py +0 -0
- {ceci-2.2 → ceci-2.3}/ceci/update_pipelines_for_ceci_2.py +0 -0
- {ceci-2.2 → ceci-2.3}/ceci/utils.py +0 -0
- {ceci-2.2 → ceci-2.3}/ceci.egg-info/dependency_links.txt +0 -0
- {ceci-2.2 → ceci-2.3}/ceci.egg-info/entry_points.txt +0 -0
- {ceci-2.2 → ceci-2.3}/ceci.egg-info/top_level.txt +0 -0
- {ceci-2.2 → ceci-2.3}/ceci.png +0 -0
- {ceci-2.2 → ceci-2.3}/ceci_example/__init__.py +0 -0
- {ceci-2.2 → ceci-2.3}/ceci_example/__main__.py +0 -0
- {ceci-2.2 → ceci-2.3}/ceci_example/example_stages.py +0 -0
- {ceci-2.2 → ceci-2.3}/ceci_example/types.py +0 -0
- {ceci-2.2 → ceci-2.3}/ceci_example_namespace/example_stages.py +0 -0
- {ceci-2.2 → ceci-2.3}/do_cover.sh +0 -0
- {ceci-2.2 → ceci-2.3}/docs/Makefile +0 -0
- {ceci-2.2 → ceci-2.3}/docs/conf.py +0 -0
- {ceci-2.2 → ceci-2.3}/docs/config2.rst +0 -0
- {ceci-2.2 → ceci-2.3}/docs/index.rst +0 -0
- {ceci-2.2 → ceci-2.3}/docs/installation.rst +0 -0
- {ceci-2.2 → ceci-2.3}/docs/logo.png +0 -0
- {ceci-2.2 → ceci-2.3}/docs/overview.rst +0 -0
- {ceci-2.2 → ceci-2.3}/docs/sites.rst +0 -0
- {ceci-2.2 → ceci-2.3}/docs/tutorial.rst +0 -0
- {ceci-2.2 → ceci-2.3}/setup.cfg +0 -0
- {ceci-2.2 → ceci-2.3}/setup.py +0 -0
- {ceci-2.2 → ceci-2.3}/tests/__init__.py +0 -0
- {ceci-2.2 → ceci-2.3}/tests/config.yml +0 -0
- {ceci-2.2 → ceci-2.3}/tests/config_nm.yml +0 -0
- {ceci-2.2 → ceci-2.3}/tests/create_registry_entries.py +0 -0
- {ceci-2.2 → ceci-2.3}/tests/inputs/dm.txt +0 -0
- {ceci-2.2 → ceci-2.3}/tests/inputs/fiducial_cosmology.txt +0 -0
- {ceci-2.2 → ceci-2.3}/tests/test.hdf5 +0 -0
- {ceci-2.2 → ceci-2.3}/tests/test_cmd.py +0 -0
- {ceci-2.2 → ceci-2.3}/tests/test_dask.py +0 -0
- {ceci-2.2 → ceci-2.3}/tests/test_flow_chart.py +0 -0
- {ceci-2.2 → ceci-2.3}/tests/test_helpers.py +0 -0
- {ceci-2.2 → ceci-2.3}/tests/test_interactive.py +0 -0
- {ceci-2.2 → ceci-2.3}/tests/test_minirunner.py +0 -0
- {ceci-2.2 → ceci-2.3}/tests/test_monitor.py +0 -0
- {ceci-2.2 → ceci-2.3}/tests/test_python_paths.py +0 -0
- {ceci-2.2 → ceci-2.3}/tests/test_site.py +0 -0
- {ceci-2.2 → ceci-2.3}/tests/test_stage.py +0 -0
- {ceci-2.2 → ceci-2.3}/tests/test_update_script.py +0 -0
- {ceci-2.2 → ceci-2.3}/tests/test_utils.py +0 -0
- {ceci-2.2 → ceci-2.3}/tests/update_test_files/config.yml +0 -0
- {ceci-2.2 → ceci-2.3}/tests/update_test_files/pipeline1.yml +0 -0
- {ceci-2.2 → ceci-2.3}/tests/update_test_files/pipeline2.yml +0 -0
{ceci-2.2 → ceci-2.3}/.gitignore
RENAMED
{ceci-2.2 → ceci-2.3}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: ceci
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3
|
|
4
4
|
Summary: Lightweight pipeline engine for LSST DESC
|
|
5
5
|
Author: Joe Zuntz
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -15,14 +15,14 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
Requires-Dist: pyyaml>=5.1
|
|
18
|
+
Requires-Dist: ruamel.yaml
|
|
18
19
|
Requires-Dist: psutil
|
|
19
20
|
Requires-Dist: graphlib_backport; python_version < "3.9"
|
|
21
|
+
Requires-Dist: networkx
|
|
22
|
+
Requires-Dist: jinja2
|
|
20
23
|
Provides-Extra: parsl
|
|
21
24
|
Requires-Dist: parsl>=1.0.0; extra == "parsl"
|
|
22
25
|
Requires-Dist: flask; extra == "parsl"
|
|
23
|
-
Provides-Extra: cwl
|
|
24
|
-
Requires-Dist: cwlref-runner; extra == "cwl"
|
|
25
|
-
Requires-Dist: cwl-utils; extra == "cwl"
|
|
26
26
|
Provides-Extra: viz
|
|
27
27
|
Requires-Dist: pygraphviz; extra == "viz"
|
|
28
28
|
Provides-Extra: dask
|
|
@@ -38,8 +38,6 @@ Requires-Dist: dask[distributed]; extra == "test"
|
|
|
38
38
|
Provides-Extra: all
|
|
39
39
|
Requires-Dist: parsl>=1.0.0; extra == "all"
|
|
40
40
|
Requires-Dist: flask; extra == "all"
|
|
41
|
-
Requires-Dist: cwlref-runner; extra == "all"
|
|
42
|
-
Requires-Dist: cwl-utils; extra == "all"
|
|
43
41
|
Requires-Dist: pygraphviz; extra == "all"
|
|
44
42
|
Requires-Dist: pytest; extra == "all"
|
|
45
43
|
Requires-Dist: pytest-cov; extra == "all"
|
|
@@ -48,6 +46,8 @@ Requires-Dist: mockmpi; extra == "all"
|
|
|
48
46
|
Requires-Dist: h5py; extra == "all"
|
|
49
47
|
Requires-Dist: dask[distributed]>=2023.5.0; extra == "all"
|
|
50
48
|
Requires-Dist: dask_mpi>=2022.4.0; extra == "all"
|
|
49
|
+
Requires-Dist: jinja2; extra == "all"
|
|
50
|
+
Dynamic: license-file
|
|
51
51
|
|
|
52
52
|

|
|
53
53
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Ceci n'est pas une pipeline"""
|
|
2
|
+
|
|
3
|
+
from .stage import PipelineStage
|
|
4
|
+
from .pipeline import (
|
|
5
|
+
Pipeline,
|
|
6
|
+
MiniPipeline,
|
|
7
|
+
ParslPipeline,
|
|
8
|
+
DryRunPipeline,
|
|
9
|
+
FlowChartPipeline,
|
|
10
|
+
)
|
|
11
|
+
from .main import run_pipeline
|
|
12
|
+
import importlib.metadata
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
__version__ = importlib.metadata.metadata(__name__)["Version"]
|
|
16
|
+
except: # pragma: no cover
|
|
17
|
+
# package is not installed
|
|
18
|
+
pass
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '2.3'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 3)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = 'ged188fcdb'
|
|
@@ -3,12 +3,8 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
5
|
import argparse
|
|
6
|
-
import subprocess
|
|
7
6
|
from .pipeline import Pipeline
|
|
8
|
-
from .sites import load, set_default_site, get_default_site
|
|
9
|
-
from .utils import extra_paths
|
|
10
7
|
from ._version import __version__
|
|
11
|
-
import contextlib
|
|
12
8
|
|
|
13
9
|
# Add the current dir to the path - often very useful
|
|
14
10
|
sys.path.append(os.getcwd())
|
|
@@ -33,7 +29,15 @@ parser.add_argument(
|
|
|
33
29
|
parser.add_argument(
|
|
34
30
|
"extra_config",
|
|
35
31
|
nargs="*",
|
|
36
|
-
help="Over-ride the main pipeline yaml file e.g. launcher.name=
|
|
32
|
+
help="Over-ride the main pipeline yaml file e.g. launcher.name=parsl",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"-t",
|
|
37
|
+
"--template-parameters",
|
|
38
|
+
nargs="*",
|
|
39
|
+
help="Template parameters to use in the pipeline configuration file, e.g. -t some_directory=xyz field=north",
|
|
40
|
+
default="",
|
|
37
41
|
)
|
|
38
42
|
|
|
39
43
|
parser.add_argument(
|
|
@@ -59,54 +63,11 @@ def run_pipeline(pipe_config):
|
|
|
59
63
|
status : `int`
|
|
60
64
|
Usual unix convention of 0 -> success, non-zero is an error code
|
|
61
65
|
"""
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
status = p.run()
|
|
66
|
+
p = Pipeline.create(pipe_config)
|
|
67
|
+
status = p.run()
|
|
65
68
|
return status
|
|
66
69
|
|
|
67
70
|
|
|
68
|
-
@contextlib.contextmanager
|
|
69
|
-
def prepare_for_pipeline(pipe_config):
|
|
70
|
-
"""
|
|
71
|
-
Prepare the paths and launcher needed to read and run a pipeline.
|
|
72
|
-
"""
|
|
73
|
-
|
|
74
|
-
# Later we will add these paths to sys.path for running here,
|
|
75
|
-
# but we will also need to pass them to the sites below so that
|
|
76
|
-
# they can be added within any containers or other launchers
|
|
77
|
-
# that we use
|
|
78
|
-
paths = pipe_config.get("python_paths", [])
|
|
79
|
-
if isinstance(paths, str): # pragma: no cover
|
|
80
|
-
paths = paths.split()
|
|
81
|
-
|
|
82
|
-
# Get information (maybe the default) on the launcher we may be using
|
|
83
|
-
launcher_config = pipe_config.setdefault("launcher", {"name": "mini"})
|
|
84
|
-
site_config = pipe_config.get("site", {"name": "local"})
|
|
85
|
-
|
|
86
|
-
# Pass the paths along to the site
|
|
87
|
-
site_config["python_paths"] = paths
|
|
88
|
-
load(launcher_config, [site_config])
|
|
89
|
-
|
|
90
|
-
# Python modules in which to search for pipeline stages
|
|
91
|
-
modules = pipe_config.get("modules", "").split()
|
|
92
|
-
|
|
93
|
-
# This helps with testing
|
|
94
|
-
default_site = get_default_site()
|
|
95
|
-
|
|
96
|
-
# temporarily add the paths to sys.path,
|
|
97
|
-
# but remove them at the end
|
|
98
|
-
with extra_paths(paths):
|
|
99
|
-
|
|
100
|
-
# Import modules. We have to do this because the definitions
|
|
101
|
-
# of the stages can be inside.
|
|
102
|
-
for module in modules:
|
|
103
|
-
__import__(module)
|
|
104
|
-
|
|
105
|
-
try:
|
|
106
|
-
yield
|
|
107
|
-
finally:
|
|
108
|
-
set_default_site(default_site)
|
|
109
|
-
|
|
110
71
|
|
|
111
72
|
def main(): # pragma: no cover
|
|
112
73
|
"""Main function called when ceci is invoked on the command line"""
|
|
@@ -116,7 +77,8 @@ def main(): # pragma: no cover
|
|
|
116
77
|
if args.flow_chart:
|
|
117
78
|
args.dry_run = True
|
|
118
79
|
pipe_config = Pipeline.build_config(
|
|
119
|
-
args.pipeline_config, args.extra_config, args.dry_run, args.flow_chart
|
|
80
|
+
args.pipeline_config, args.extra_config, args.dry_run, args.flow_chart,
|
|
81
|
+
args.template_parameters
|
|
120
82
|
)
|
|
121
83
|
status = run_pipeline(pipe_config)
|
|
122
84
|
|
|
@@ -377,7 +377,7 @@ class Runner:
|
|
|
377
377
|
# status==0 indicates sucess in job, so free resources
|
|
378
378
|
else:
|
|
379
379
|
run_time = make_run_time_string(default_timer() - start_time)
|
|
380
|
-
print(f"Job {job.name} has completed successfully in {run_time}
|
|
380
|
+
print(f"Job {job.name} has completed successfully in {run_time}")
|
|
381
381
|
# Call back with info about the successful job and how it ran
|
|
382
382
|
self.callback(
|
|
383
383
|
EVENT_COMPLETED,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Module with core pipeline functionality"""
|
|
2
|
+
|
|
3
|
+
from .pipeline import (
|
|
4
|
+
Pipeline,
|
|
5
|
+
override_config,
|
|
6
|
+
RESUME_MODE_RESUME,
|
|
7
|
+
RESUME_MODE_RESTART,
|
|
8
|
+
RESUME_MODE_REFUSE,
|
|
9
|
+
)
|
|
10
|
+
from .parsl import ParslPipeline
|
|
11
|
+
from .dry_run import DryRunPipeline
|
|
12
|
+
from .flow_chart import FlowChartPipeline
|
|
13
|
+
from .mini import MiniPipeline
|
|
14
|
+
from .file_manager import FileManager
|
|
15
|
+
from .sec import StageExecutionConfig
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from .pipeline import Pipeline
|
|
3
|
+
from ..utils import embolden
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DryRunPipeline(Pipeline):
|
|
7
|
+
"""A pipeline subclass which just does a dry-run, showing which commands
|
|
8
|
+
would be executed.
|
|
9
|
+
|
|
10
|
+
See the base class for almost all behaviour.
|
|
11
|
+
|
|
12
|
+
No additional attributes.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def initiate_run(self, overall_inputs):
|
|
16
|
+
return []
|
|
17
|
+
|
|
18
|
+
def should_skip_stage(self, stage):
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
def enqueue_job(self, stage, pipeline_files):
|
|
22
|
+
outputs = stage.find_outputs(self.run_config["output_dir"])
|
|
23
|
+
sec = self.stage_execution_config[stage.instance_name]
|
|
24
|
+
|
|
25
|
+
cmd = sec.generate_full_command(pipeline_files, outputs, self.stages_config)
|
|
26
|
+
|
|
27
|
+
# Replace the first instance of the stage name with bold
|
|
28
|
+
# text, but only if we are printing to screen. This helps the
|
|
29
|
+
# eye pick out the stage you want to run.
|
|
30
|
+
if sys.stdout.isatty():
|
|
31
|
+
cmd = cmd.replace(stage.instance_name, embolden(stage.instance_name), 1)
|
|
32
|
+
|
|
33
|
+
self.run_info.append(cmd)
|
|
34
|
+
return outputs
|
|
35
|
+
|
|
36
|
+
def run_jobs(self):
|
|
37
|
+
for cmd in self.run_info:
|
|
38
|
+
print(cmd)
|
|
39
|
+
print("\n")
|
|
40
|
+
return 0
|
|
41
|
+
|
|
42
|
+
def find_all_outputs(self):
|
|
43
|
+
return {}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
class FileManager(dict):
|
|
2
|
+
"""Small class to manage files within a particular Pipeline
|
|
3
|
+
|
|
4
|
+
This is a dict which is used for tag to path mapping,
|
|
5
|
+
but has a couple of additional dicts to manage the reverse mapping and the
|
|
6
|
+
tag to type mapping.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
The tag to path mapping is the thing that the Pipeline uses to set the
|
|
10
|
+
input paths for downstream stages that use the outputs of earlier stages,
|
|
11
|
+
i.e., everything in the pipeline can refer to a particular file by tag.
|
|
12
|
+
|
|
13
|
+
The tag defaults to the input or output tag as define in the stage class attributes.
|
|
14
|
+
However, in the case that we want multiple stages of the same class in a pipeline we
|
|
15
|
+
have to alias the tags so that each stage can write to its own location (and so that
|
|
16
|
+
downstream stages can pick up that location correctly)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
"""Constructor, makes empty dictionaries"""
|
|
21
|
+
self._tag_to_type = {}
|
|
22
|
+
self._path_to_tag = {}
|
|
23
|
+
dict.__init__(self)
|
|
24
|
+
|
|
25
|
+
def __setitem__(self, tag, path):
|
|
26
|
+
"""Override dict.__setitem__() to also insert the reverse mapping"""
|
|
27
|
+
dict.__setitem__(self, tag, path)
|
|
28
|
+
self._path_to_tag[path] = tag
|
|
29
|
+
|
|
30
|
+
def insert(self, tag, path=None, ftype=None):
|
|
31
|
+
"""Insert a file, including the path and the file type
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
tag : str
|
|
36
|
+
The tag by which this file will be identified
|
|
37
|
+
path : str
|
|
38
|
+
The path to this file
|
|
39
|
+
ftype : type
|
|
40
|
+
The file type for this file
|
|
41
|
+
"""
|
|
42
|
+
if path is not None:
|
|
43
|
+
self[tag] = path
|
|
44
|
+
self._path_to_tag[path] = tag
|
|
45
|
+
if tag not in self._tag_to_type:
|
|
46
|
+
self._tag_to_type[tag] = ftype
|
|
47
|
+
|
|
48
|
+
def get_type(self, tag):
|
|
49
|
+
"""Return the file type associated to a given tag"""
|
|
50
|
+
return self._tag_to_type[tag]
|
|
51
|
+
|
|
52
|
+
def get_path(self, tag):
|
|
53
|
+
"""Return the path associated to a give tag"""
|
|
54
|
+
return self[tag]
|
|
55
|
+
|
|
56
|
+
def get_tag(self, path):
|
|
57
|
+
"""Return the tag associated to a given path"""
|
|
58
|
+
return self._path_to_tag[path]
|
|
59
|
+
|
|
60
|
+
def insert_paths(self, path_dict):
|
|
61
|
+
"""Insert a set of paths from a dict that has tag, path pairs"""
|
|
62
|
+
for key, val in path_dict.items():
|
|
63
|
+
self.insert(key, path=val)
|
|
64
|
+
|
|
65
|
+
def insert_outputs(self, stage, outdir):
|
|
66
|
+
"""Insert a set of tags and associated paths and file types that are output by a stage"""
|
|
67
|
+
stage_outputs = stage.find_outputs(outdir)
|
|
68
|
+
for tag, ftype in stage.outputs:
|
|
69
|
+
aliased_tag = stage.get_aliased_tag(tag)
|
|
70
|
+
path = stage_outputs[aliased_tag]
|
|
71
|
+
self.insert(aliased_tag, path=path, ftype=ftype)
|
|
72
|
+
return stage_outputs
|
|
73
|
+
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import networkx
|
|
2
|
+
import collections
|
|
3
|
+
|
|
4
|
+
def build_graph(stage_names, stage_classes, stage_aliases, overall_inputs):
|
|
5
|
+
"""
|
|
6
|
+
Build a directed graph of the pipeline stages and their inputs and outputs.
|
|
7
|
+
|
|
8
|
+
This also checks for various pipeline problems like missing inputs,
|
|
9
|
+
multiple stages generating the same output, and cyclic dependencies.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
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
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
graph : `networkx.DiGraph`
|
|
25
|
+
A directed graph representing the pipeline stages, their inputs, and outputs
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# Build a graph with all the stages and files in it
|
|
29
|
+
graph = networkx.DiGraph()
|
|
30
|
+
|
|
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:
|
|
60
|
+
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():
|
|
63
|
+
tag = aliases.get(tag, tag)
|
|
64
|
+
if tag not in graph.nodes:
|
|
65
|
+
missing[tag].append(stage_name)
|
|
66
|
+
else:
|
|
67
|
+
graph.add_edge(tag, stage_name)
|
|
68
|
+
|
|
69
|
+
if missing:
|
|
70
|
+
msg = []
|
|
71
|
+
for tag, stages in missing.items():
|
|
72
|
+
stages = ", ".join(stages)
|
|
73
|
+
msg.append(f"- {tag} - needed by stages {stages}")
|
|
74
|
+
msg = "\n.join(msg)"
|
|
75
|
+
raise ValueError(f"The following files are needed by the pipeline but not generated by any stage:\n{msg}")
|
|
76
|
+
|
|
77
|
+
# Now check for cycles in the graph
|
|
78
|
+
cycles = list(networkx.simple_cycles(graph))
|
|
79
|
+
if cycles:
|
|
80
|
+
msg = []
|
|
81
|
+
for cycle in cycles:
|
|
82
|
+
cycle_text = " -> ".join(cycle)
|
|
83
|
+
cycle_text += " -> " + cycle[0]
|
|
84
|
+
msg.append(f"- {cycle_text}")
|
|
85
|
+
raise ValueError(f"Your pipeline is cyclic. The following cycles were found:\n{msg}")
|
|
86
|
+
|
|
87
|
+
return graph
|
|
88
|
+
|
|
89
|
+
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))
|
|
93
|
+
|
|
94
|
+
# cut down to just the stages
|
|
95
|
+
ordering = [node for node in ordering if graph.nodes[node]["type"] == "stage"]
|
|
96
|
+
|
|
97
|
+
return ordering
|
|
98
|
+
|
|
99
|
+
def trim_pipeline_graph(graph, from_=None, to_=None):
|
|
100
|
+
"""
|
|
101
|
+
Trim the pipeline graph to only include the stages between `from_` and `to_`.
|
|
102
|
+
|
|
103
|
+
Either `from_` or `to_` can be `None`, in which case no trimming is done in that direction.
|
|
104
|
+
|
|
105
|
+
They can be single strings, or lists of strings, in which case the graph is trimmed to include all stages
|
|
106
|
+
between any of the stages in `from_` and any of the stages in `to
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
graph : `networkx.DiGraph`
|
|
111
|
+
The original pipeline graph
|
|
112
|
+
from_ : `str`
|
|
113
|
+
The name of the stage(s) or file(s) to start from.
|
|
114
|
+
to_ : `str`
|
|
115
|
+
The name of the stage(s) or file(s) to to end at.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
trimmed_graph : `networkx.DiGraph`
|
|
120
|
+
The trimmed pipeline graph
|
|
121
|
+
"""
|
|
122
|
+
if isinstance(from_, str):
|
|
123
|
+
from_ = [from_]
|
|
124
|
+
if isinstance(to_, str):
|
|
125
|
+
to_ = [to_]
|
|
126
|
+
|
|
127
|
+
# Descendents is all nodes that are descendend from any of the nodes in `from_`
|
|
128
|
+
# if from_ is None then it includes all nodes in the graph
|
|
129
|
+
descendents = set()
|
|
130
|
+
|
|
131
|
+
if from_ is None:
|
|
132
|
+
descendents.update(graph.nodes)
|
|
133
|
+
else:
|
|
134
|
+
for node in from_:
|
|
135
|
+
if node not in graph.nodes:
|
|
136
|
+
raise ValueError(f"Node {node} is not in the pipeline graph")
|
|
137
|
+
# first include the nodes in `from_` themselves
|
|
138
|
+
descendents.add(node)
|
|
139
|
+
descendents.update(networkx.descendants(graph, node))
|
|
140
|
+
|
|
141
|
+
# Ancestors is all nodes that are ancestors of any of the nodes in `to_`
|
|
142
|
+
# if to_ is None then it includes all nodes in the graph
|
|
143
|
+
ancestors = set()
|
|
144
|
+
if to_ is None:
|
|
145
|
+
ancestors.update(graph.nodes)
|
|
146
|
+
else:
|
|
147
|
+
for node in to_:
|
|
148
|
+
if node not in graph.nodes:
|
|
149
|
+
raise ValueError(f"Node {node} is not in the pipeline graph")
|
|
150
|
+
ancestors.add(node)
|
|
151
|
+
ancestors.update(networkx.ancestors(graph, node))
|
|
152
|
+
|
|
153
|
+
# Now we want to keep everything that is both a descendent of `from_` and an ancestor of `to_`
|
|
154
|
+
# This is the intersection of the two sets
|
|
155
|
+
nodes_to_keep = descendents.intersection(ancestors)
|
|
156
|
+
|
|
157
|
+
# We also want to keep the input and output files for the stages we are keeping
|
|
158
|
+
# (though not the stages that make those)
|
|
159
|
+
more_to_keep = set()
|
|
160
|
+
for node in nodes_to_keep:
|
|
161
|
+
if graph.nodes[node]["type"] == "stage":
|
|
162
|
+
# Get all the outputs of this stage
|
|
163
|
+
outputs = [n for n in graph.successors(node) if graph.nodes[n]["type"] == "output"]
|
|
164
|
+
more_to_keep.update(outputs)
|
|
165
|
+
# same for the inputs to all the stages
|
|
166
|
+
inputs = [n for n in graph.predecessors(node) if graph.nodes[n]["type"] in ["input", "output"]]
|
|
167
|
+
more_to_keep.update(inputs)
|
|
168
|
+
nodes_to_keep.update(more_to_keep)
|
|
169
|
+
|
|
170
|
+
# Create a subgraph with just these nodes
|
|
171
|
+
trimmed_graph = graph.subgraph(nodes_to_keep).copy()
|
|
172
|
+
|
|
173
|
+
# Some stages that were previously outputs of existing stages
|
|
174
|
+
# must now be overall inputs to the pipeline instead because
|
|
175
|
+
# the stage that generated them has been trimmed. We need to keep
|
|
176
|
+
# track of this
|
|
177
|
+
converted_inputs = {}
|
|
178
|
+
for node_name in list(trimmed_graph.nodes):
|
|
179
|
+
node = trimmed_graph.nodes[node_name]
|
|
180
|
+
|
|
181
|
+
if node["type"] == "output":
|
|
182
|
+
# The node will be listed as an output but will have no
|
|
183
|
+
# 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")
|
|
195
|
+
# the node_name is the aliased_tag, so this file name should be correct
|
|
196
|
+
file_name = ftype.make_name(node_name)
|
|
197
|
+
converted_inputs[node_name] = file_name
|
|
198
|
+
|
|
199
|
+
return trimmed_graph, converted_inputs
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from .. import minirunner
|
|
3
|
+
from .pipeline import Pipeline, get_default_site
|
|
4
|
+
|
|
5
|
+
class MiniPipeline(Pipeline):
|
|
6
|
+
"""A pipeline subclass that uses Minirunner, a sub-module
|
|
7
|
+
of ceci, to run.
|
|
8
|
+
|
|
9
|
+
Minirununer is a small tool I wrote suitable for interactive
|
|
10
|
+
jobs on cori. It launches jobs locally (not through a batch system),
|
|
11
|
+
which parsl can also do, but it has a simple and clearer (to me at least)
|
|
12
|
+
understanding of available nodes and cores.
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, *args, **kwargs):
|
|
17
|
+
"""Create a MiniRunner Pipeline
|
|
18
|
+
|
|
19
|
+
In addition to parent initialization parameters (see the
|
|
20
|
+
Pipeline base class), this subclass can take these optional
|
|
21
|
+
keywords.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
callback: function(event_type: str, event_info: dict)
|
|
26
|
+
A function called when jobs launch, complete, or fail,
|
|
27
|
+
and when the pipeline aborts. Can be used for tracing
|
|
28
|
+
execution. Default=None.
|
|
29
|
+
|
|
30
|
+
sleep: function(t: float)
|
|
31
|
+
A function to replace time.sleep called in the pipeline
|
|
32
|
+
to wait until the next time to check process completion
|
|
33
|
+
Most normal usage will not need this. Default=None.
|
|
34
|
+
"""
|
|
35
|
+
self.callback = kwargs.pop("callback", None)
|
|
36
|
+
self.sleep = kwargs.pop("sleep", None)
|
|
37
|
+
super().__init__(*args, **kwargs)
|
|
38
|
+
|
|
39
|
+
def build_dag(self, jobs):
|
|
40
|
+
"""Build a directed acyclic graph of a set of stages.
|
|
41
|
+
|
|
42
|
+
The DAG is represented by a list of jobs that each other job
|
|
43
|
+
depends on. If all a job's dependencies are complete
|
|
44
|
+
then it can be run.
|
|
45
|
+
|
|
46
|
+
Fun fact: the word "dag" is also Australian slang for a
|
|
47
|
+
"dung-caked lock of wool around the hindquarters of a sheep".
|
|
48
|
+
and is used as a semi-affectionate insult.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
|
|
53
|
+
stages: list[PipelineStage]
|
|
54
|
+
A list of stages to generate the DAG for
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
depend = {}
|
|
58
|
+
|
|
59
|
+
# for each stage in our pipeline ...
|
|
60
|
+
for stage in self.stages[:]:
|
|
61
|
+
if stage.instance_name not in jobs:
|
|
62
|
+
continue
|
|
63
|
+
job = jobs[stage.instance_name]
|
|
64
|
+
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])
|
|
78
|
+
return depend
|
|
79
|
+
|
|
80
|
+
def initiate_run(self, overall_inputs):
|
|
81
|
+
jobs = {}
|
|
82
|
+
stages = []
|
|
83
|
+
return jobs, stages
|
|
84
|
+
|
|
85
|
+
def enqueue_job(self, stage, pipeline_files):
|
|
86
|
+
sec = self.stage_execution_config[stage.instance_name]
|
|
87
|
+
outputs = stage.find_outputs(self.run_config["output_dir"])
|
|
88
|
+
cmd = sec.generate_full_command(pipeline_files, outputs, self.stages_config)
|
|
89
|
+
job = minirunner.Job(
|
|
90
|
+
stage.instance_name,
|
|
91
|
+
cmd,
|
|
92
|
+
cores=sec.threads_per_process * sec.nprocess,
|
|
93
|
+
nodes=sec.nodes,
|
|
94
|
+
)
|
|
95
|
+
self.run_info[0][stage.instance_name] = job
|
|
96
|
+
self.run_info[1].append(stage)
|
|
97
|
+
return outputs
|
|
98
|
+
|
|
99
|
+
def run_jobs(self):
|
|
100
|
+
jobs, _ = self.run_info
|
|
101
|
+
# Now the jobs have all been queued, build them into a graph
|
|
102
|
+
graph = self.build_dag(jobs)
|
|
103
|
+
nodes = get_default_site().info["nodes"]
|
|
104
|
+
log_dir = self.run_config["log_dir"]
|
|
105
|
+
|
|
106
|
+
# This pipeline, being mainly for testing, can only
|
|
107
|
+
# run at a single site, so we can assume here that the
|
|
108
|
+
# sites are all the same
|
|
109
|
+
sec = self.stage_execution_config[self.stage_names[0]]
|
|
110
|
+
nodes = sec.site.info["nodes"]
|
|
111
|
+
|
|
112
|
+
# Run under minirununer
|
|
113
|
+
runner = minirunner.Runner(
|
|
114
|
+
nodes, graph, log_dir, callback=self.callback, sleep=self.sleep
|
|
115
|
+
)
|
|
116
|
+
interval = self.launcher_config.get("interval", 3)
|
|
117
|
+
try:
|
|
118
|
+
runner.run(interval)
|
|
119
|
+
except minirunner.FailedJob as error:
|
|
120
|
+
sys.stderr.write(
|
|
121
|
+
f"""
|
|
122
|
+
*************************************************
|
|
123
|
+
Error running pipeline stage {error.job_name}.
|
|
124
|
+
Failed after {error.run_time}.
|
|
125
|
+
|
|
126
|
+
Standard output and error streams in {log_dir}/{error.job_name}.out
|
|
127
|
+
*************************************************
|
|
128
|
+
"""
|
|
129
|
+
)
|
|
130
|
+
return 1
|
|
131
|
+
|
|
132
|
+
return 0
|
|
133
|
+
|