shexec 2.2.0__py3-none-any.whl
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.
- shexec/__init__.py +52 -0
- shexec/__main__.py +156 -0
- shexec/models.py +152 -0
- shexec/plugin.py +144 -0
- shexec/shexec.py +105 -0
- shexec-2.2.0.dist-info/METADATA +190 -0
- shexec-2.2.0.dist-info/RECORD +11 -0
- shexec-2.2.0.dist-info/WHEEL +5 -0
- shexec-2.2.0.dist-info/entry_points.txt +2 -0
- shexec-2.2.0.dist-info/licenses/LICENSE +21 -0
- shexec-2.2.0.dist-info/top_level.txt +1 -0
shexec/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""shexec: a ChRIS DS plugin that runs a shell-type command over every file of a tree.
|
|
2
|
+
|
|
3
|
+
Key components:
|
|
4
|
+
- plugin_build, PluginSpec, Plugin: make a ChRIS plugin from a spec; what a
|
|
5
|
+
downstream plugin calls
|
|
6
|
+
- ShExec: pfdo_run's engine, built from the plugin's arguments
|
|
7
|
+
- ShExecOptions: the options
|
|
8
|
+
- plugin_run: the body of the plugin's main
|
|
9
|
+
- the records of pfdo_run and pflog, re-exported through shexec.models
|
|
10
|
+
|
|
11
|
+
Dependencies:
|
|
12
|
+
- chris_plugin, pfdo_run, pflog
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
__version__: str = "2.2.0"
|
|
18
|
+
|
|
19
|
+
from shexec.models import (
|
|
20
|
+
DirNode,
|
|
21
|
+
FileJob,
|
|
22
|
+
HostFacts,
|
|
23
|
+
JobResult,
|
|
24
|
+
JobSpec,
|
|
25
|
+
Plugin,
|
|
26
|
+
PluginSpec,
|
|
27
|
+
RunResult,
|
|
28
|
+
ShExecOptions,
|
|
29
|
+
Span,
|
|
30
|
+
StageResult,
|
|
31
|
+
)
|
|
32
|
+
from shexec.plugin import plugin_build
|
|
33
|
+
from shexec.shexec import ShExec, options_build, plugin_run
|
|
34
|
+
|
|
35
|
+
__all__: list[str] = [
|
|
36
|
+
"DirNode",
|
|
37
|
+
"FileJob",
|
|
38
|
+
"HostFacts",
|
|
39
|
+
"JobResult",
|
|
40
|
+
"JobSpec",
|
|
41
|
+
"Plugin",
|
|
42
|
+
"PluginSpec",
|
|
43
|
+
"RunResult",
|
|
44
|
+
"ShExec",
|
|
45
|
+
"ShExecOptions",
|
|
46
|
+
"Span",
|
|
47
|
+
"StageResult",
|
|
48
|
+
"__version__",
|
|
49
|
+
"options_build",
|
|
50
|
+
"plugin_build",
|
|
51
|
+
"plugin_run",
|
|
52
|
+
]
|
shexec/__main__.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""The ``shexec`` command: a ChRIS DS plugin that runs a command over every file of a tree.
|
|
2
|
+
|
|
3
|
+
This module is shexec's own call of ``plugin_build``: its name, its description for
|
|
4
|
+
ChRIS, no flag defaults, and its words on the manual page. A plugin built on shexec
|
|
5
|
+
has a module of the same shape with its own spec; see ``shexec.plugin`` and the
|
|
6
|
+
README's "Building a plugin on pl-shexec".
|
|
7
|
+
|
|
8
|
+
Key components:
|
|
9
|
+
- SPEC: what shexec says about itself
|
|
10
|
+
- PLUGIN: the built plugin
|
|
11
|
+
- parser, main: the plugin's flags and the console script's target
|
|
12
|
+
|
|
13
|
+
Typical usage:
|
|
14
|
+
shexec --fileFilter 'glob:*.jpg' --exec "convert %{inputWorkingDir}/%{inputWorkingFile}
|
|
15
|
+
%{outputWorkingDir}/%{inputWorkingFile|rmext}.png" /incoming /outgoing
|
|
16
|
+
|
|
17
|
+
Dependencies:
|
|
18
|
+
- shexec.plugin, shexec.models
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
|
|
26
|
+
from pftree.manual import Manual
|
|
27
|
+
|
|
28
|
+
from shexec import __version__
|
|
29
|
+
from shexec.models import Plugin, PluginSpec
|
|
30
|
+
from shexec.plugin import plugin_build
|
|
31
|
+
|
|
32
|
+
TITLE: str = "Execute shell-type commands across input spaces"
|
|
33
|
+
CATEGORY: str = "Utility"
|
|
34
|
+
MIN_MEMORY: str = "2Gi"
|
|
35
|
+
MIN_CPU: str = "1000m"
|
|
36
|
+
|
|
37
|
+
BANNER_ART: str = r"""
|
|
38
|
+
_ _
|
|
39
|
+
| | | |
|
|
40
|
+
_ __ | |______ ___| |__ _____ _____ ___
|
|
41
|
+
| '_ \| |______/ __| '_ \ / _ \ \/ / _ \/ __|
|
|
42
|
+
| |_) | | \__ \ | | | __/> < __/ (__
|
|
43
|
+
| .__/|_| |___/_| |_|\___/_/\_\___|\___|
|
|
44
|
+
| |
|
|
45
|
+
|_|
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
BANNER_TITLE: str = "Run shell-type commands over an input tree"
|
|
49
|
+
|
|
50
|
+
BANNER_TEXT: str = """
|
|
51
|
+
Run shell-type commands over an input tree
|
|
52
|
+
|
|
53
|
+
-- version {version} --
|
|
54
|
+
|
|
55
|
+
A ChRIS DS plugin that runs one command for every selected file of
|
|
56
|
+
every directory in its input tree, the command written as a template
|
|
57
|
+
whose %{{...}} tags name the file and directories in hand. The output
|
|
58
|
+
tree mirrors the input tree; each command's record lands beside its
|
|
59
|
+
results. The engine is pfdo_run.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
DESCRIPTION: str = """
|
|
63
|
+
DESCRIPTION
|
|
64
|
+
|
|
65
|
+
``shexec`` is pfdo_run as a ChRIS plugin. ChRIS gives it an input and
|
|
66
|
+
an output directory; every other flag is pfdo_run's. The input tree is
|
|
67
|
+
walked, files are selected in each directory by the filters and
|
|
68
|
+
--analyzeFileIndex, and the --exec template is run once per selected
|
|
69
|
+
file with its tags resolved against that directory and file, without
|
|
70
|
+
a shell. Each job's record, the command, its output, its exit status,
|
|
71
|
+
the host and the timing, is one line of JSON in jobs.jsonl in its
|
|
72
|
+
output directory unless --noJobLogging is given; --jobFiles adds the
|
|
73
|
+
four files beside the result; at --verbosity 2 a job's output is also
|
|
74
|
+
streamed to the console as it runs.
|
|
75
|
+
|
|
76
|
+
The plugin's own run is timed: one line of JSON on standard error, when
|
|
77
|
+
it began, when it ended, the seconds, and whether it succeeded. CUBE
|
|
78
|
+
keeps that line with the rest of the plugin's output.
|
|
79
|
+
|
|
80
|
+
The exit status is 0 when at least one directory's jobs all succeeded,
|
|
81
|
+
1 when every directory failed or the run could not start, 2 when --exec
|
|
82
|
+
is missing. The commands available are those of the container image:
|
|
83
|
+
the coreutils, zip and unzip, bc, perl, tar, ImageMagick's convert,
|
|
84
|
+
and the DCMTK tools.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
EXAMPLES: str = """
|
|
88
|
+
|
|
89
|
+
EXAMPLES
|
|
90
|
+
|
|
91
|
+
Convert every jpg in the input tree to png in the output tree:
|
|
92
|
+
|
|
93
|
+
shexec \\
|
|
94
|
+
--fileFilter 'glob:*.jpg' \\
|
|
95
|
+
--exec "convert %{inputWorkingDir}/%{inputWorkingFile}
|
|
96
|
+
%{outputWorkingDir}/%{inputWorkingFile|rmext}.png" \\
|
|
97
|
+
--threads 4 /incoming /outgoing
|
|
98
|
+
|
|
99
|
+
Each conversion's command, stdout, stderr, return code and timing are one
|
|
100
|
+
line in the directory's jobs.jsonl; ``pflog read /outgoing/**/jobs.jsonl``
|
|
101
|
+
lists them.
|
|
102
|
+
|
|
103
|
+
Copy one file out of each directory whose path names a subject:
|
|
104
|
+
|
|
105
|
+
shexec \\
|
|
106
|
+
--dirFilter 100307 \\
|
|
107
|
+
--exec "cp %{inputWorkingDir}/brain.mgz %{outputWorkingDir}/brain.mgz" \\
|
|
108
|
+
--analyzeFileIndex f --noJobLogging /incoming /outgoing
|
|
109
|
+
|
|
110
|
+
Zip each directory's files into one archive named after the directory:
|
|
111
|
+
|
|
112
|
+
shexec \\
|
|
113
|
+
--analyzeFileIndex f \\
|
|
114
|
+
--exec "zip -j %{outputWorkingDir}/%{inputWorkingDir|basename}.zip
|
|
115
|
+
%{inputWorkingDir}/%{inputWorkingFile}" \\
|
|
116
|
+
/incoming /outgoing
|
|
117
|
+
|
|
118
|
+
From the image, with the directories mounted:
|
|
119
|
+
|
|
120
|
+
docker run --rm -v $PWD/in:/incoming:ro -v $PWD/out:/outgoing:rw \\
|
|
121
|
+
fnndsc/pl-shexec shexec --fileFilter txt \\
|
|
122
|
+
--exec "wc -l %{inputWorkingDir}/%{inputWorkingFile}" /incoming /outgoing
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
BRIEF_EXAMPLE: str = """
|
|
127
|
+
|
|
128
|
+
shexec --fileFilter 'glob:*.jpg' \\
|
|
129
|
+
--exec "convert %{inputWorkingDir}/%{inputWorkingFile}
|
|
130
|
+
%{outputWorkingDir}/%{inputWorkingFile|rmext}.png" \\
|
|
131
|
+
/incoming /outgoing
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
SPEC: PluginSpec = PluginSpec(
|
|
135
|
+
name="shexec",
|
|
136
|
+
version=__version__,
|
|
137
|
+
title=TITLE,
|
|
138
|
+
category=CATEGORY,
|
|
139
|
+
minMemory=MIN_MEMORY,
|
|
140
|
+
minCpu=MIN_CPU,
|
|
141
|
+
bannerArt=BANNER_ART,
|
|
142
|
+
bannerTitle=BANNER_TITLE,
|
|
143
|
+
bannerText=BANNER_TEXT,
|
|
144
|
+
briefExample=BRIEF_EXAMPLE,
|
|
145
|
+
description=DESCRIPTION,
|
|
146
|
+
examples=EXAMPLES,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
PLUGIN: Plugin = plugin_build(SPEC)
|
|
150
|
+
parser: argparse.ArgumentParser = PLUGIN.parser
|
|
151
|
+
main: Callable[..., int] = PLUGIN.main
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def manual_build() -> Manual:
|
|
155
|
+
"""The shexec manual page."""
|
|
156
|
+
return PLUGIN.manual
|
shexec/models.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""The records shexec works with: its options, and everything pfdo_run's records carry.
|
|
2
|
+
|
|
3
|
+
A program built on shexec imports every record from here, and reaches past it into
|
|
4
|
+
pfdo_run, pfdo, pftree, hostlib or pflog for nothing.
|
|
5
|
+
|
|
6
|
+
Key components:
|
|
7
|
+
- ShExecOptions: pfdo_run's options, for the plugin
|
|
8
|
+
- PluginSpec: what a plugin built on shexec says about itself
|
|
9
|
+
- Plugin: what ``plugin_build`` returns
|
|
10
|
+
- DirNode, FileJob, JobResult, JobSpec, HostFacts, RunResult, StageResult,
|
|
11
|
+
ProcessResult, TreeOptions, PfdoOptions, PfdoRunOptions: re-exported
|
|
12
|
+
- Span: pflog's record of the plugin's run, re-exported
|
|
13
|
+
|
|
14
|
+
Dependencies:
|
|
15
|
+
- pfdo_run.models, pflog.models
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
from collections.abc import Callable
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from pfdo_run.models import (
|
|
26
|
+
DirNode,
|
|
27
|
+
FileJob,
|
|
28
|
+
HostFacts,
|
|
29
|
+
JobResult,
|
|
30
|
+
JobSpec,
|
|
31
|
+
PfdoOptions,
|
|
32
|
+
PfdoRunOptions,
|
|
33
|
+
ProcessResult,
|
|
34
|
+
RunResult,
|
|
35
|
+
StageResult,
|
|
36
|
+
TreeOptions,
|
|
37
|
+
)
|
|
38
|
+
from pflog.models import Span
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class ShExecOptions(PfdoRunOptions):
|
|
43
|
+
"""What one run of the plugin is told.
|
|
44
|
+
|
|
45
|
+
The same options as pfdo_run, built from the plugin's positional directories
|
|
46
|
+
and its flags. The class exists so a plugin built on shexec has a place to add
|
|
47
|
+
its own.
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> ShExecOptions.fromDict({"inputDir": "/in", "outputDir": "/out", "exec": "ls"}).exec
|
|
51
|
+
'ls'
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
DEFAULT_BANNER_ART: str = r"""
|
|
56
|
+
_ _
|
|
57
|
+
| | | |
|
|
58
|
+
_ __ | |______ ___| |__ _____ _____ ___
|
|
59
|
+
| '_ \| |______/ __| '_ \ / _ \ \/ / _ \/ __|
|
|
60
|
+
| |_) | | \__ \ | | | __/> < __/ (__
|
|
61
|
+
| .__/|_| |___/_| |_|\___/_/\_\___|\___|
|
|
62
|
+
| |
|
|
63
|
+
|_|
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class PluginSpec:
|
|
69
|
+
"""What a plugin built on shexec says about itself.
|
|
70
|
+
|
|
71
|
+
Everything a downstream plugin changes from shexec is here; the engine, the
|
|
72
|
+
parser's flags, the records and the exit status are not, because they are the
|
|
73
|
+
same for every plugin on the stack.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
name: the command and the span, ``shexec``, ``freesurfer``.
|
|
77
|
+
version: the plugin's version, for ``--version`` and the description.
|
|
78
|
+
title: the plugin's title in ChRIS.
|
|
79
|
+
category: the plugin's category in ChRIS.
|
|
80
|
+
minMemory: the memory ChRIS reserves, as ``2Gi``.
|
|
81
|
+
minCpu: the CPU ChRIS reserves, in millicores, as ``1000m``.
|
|
82
|
+
minGpu: GPUs required; 0 for none.
|
|
83
|
+
defaults: flag defaults, ``exec`` above all; a derived plugin's usual command.
|
|
84
|
+
bannerArt: the ASCII art on the manual's first page.
|
|
85
|
+
bannerTitle: the one-line title under it.
|
|
86
|
+
bannerText: the banner prose; ``{version}`` in it is replaced.
|
|
87
|
+
briefExample: the manual's BRIEF EXAMPLE, an indented command.
|
|
88
|
+
description: the manual's DESCRIPTION section, heading included.
|
|
89
|
+
examples: the manual's EXAMPLES section, heading included.
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
>>> PluginSpec(name="freesurfer", version="1.0.0", defaults={"exec": "recon-all"}).defaults["exec"]
|
|
93
|
+
'recon-all'
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
name: str
|
|
97
|
+
version: str
|
|
98
|
+
title: str = ""
|
|
99
|
+
category: str = "Utility"
|
|
100
|
+
minMemory: str = "2Gi"
|
|
101
|
+
minCpu: str = "1000m"
|
|
102
|
+
minGpu: int = 0
|
|
103
|
+
defaults: dict[str, Any] = field(default_factory=dict)
|
|
104
|
+
bannerArt: str = DEFAULT_BANNER_ART
|
|
105
|
+
bannerTitle: str = ""
|
|
106
|
+
bannerText: str = ""
|
|
107
|
+
briefExample: str = ""
|
|
108
|
+
description: str = ""
|
|
109
|
+
examples: str = ""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class Plugin:
|
|
114
|
+
"""What ``plugin_build`` returns: a plugin ready to be a console script.
|
|
115
|
+
|
|
116
|
+
Attributes:
|
|
117
|
+
name: the plugin's name.
|
|
118
|
+
spec: what it was built from.
|
|
119
|
+
parser: its flags, defaults applied; for ``chris_plugin`` and for tests.
|
|
120
|
+
main: the decorated main, the console script's target.
|
|
121
|
+
manual: its manual page.
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
>>> Plugin.__dataclass_fields__["main"].name
|
|
125
|
+
'main'
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
name: str
|
|
129
|
+
spec: PluginSpec
|
|
130
|
+
parser: argparse.ArgumentParser
|
|
131
|
+
main: Callable[..., int]
|
|
132
|
+
manual: Any
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
__all__: list[str] = [
|
|
136
|
+
"DEFAULT_BANNER_ART",
|
|
137
|
+
"DirNode",
|
|
138
|
+
"FileJob",
|
|
139
|
+
"HostFacts",
|
|
140
|
+
"JobResult",
|
|
141
|
+
"JobSpec",
|
|
142
|
+
"PfdoOptions",
|
|
143
|
+
"PfdoRunOptions",
|
|
144
|
+
"Plugin",
|
|
145
|
+
"PluginSpec",
|
|
146
|
+
"ProcessResult",
|
|
147
|
+
"RunResult",
|
|
148
|
+
"ShExecOptions",
|
|
149
|
+
"Span",
|
|
150
|
+
"StageResult",
|
|
151
|
+
"TreeOptions",
|
|
152
|
+
]
|
shexec/plugin.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Making a ChRIS plugin out of shexec: the factory a downstream plugin calls.
|
|
2
|
+
|
|
3
|
+
A plugin that wraps a command-line program is shexec with four things changed: its
|
|
4
|
+
name and version, its description for ChRIS, the defaults of its flags, above all the
|
|
5
|
+
``--exec`` template, and the words on its manual page. ``plugin_build`` takes those as
|
|
6
|
+
a ``PluginSpec`` and returns a ``Plugin``: the parser with the defaults applied, the
|
|
7
|
+
manual composed from the plugin's own banner over pfdo_run's shared sections, and
|
|
8
|
+
``main`` already wrapped in pflog's ``timed`` and ``chris_plugin``. pl-shexec itself is
|
|
9
|
+
the first call of it; pl-freesurfer is the second, with FreeSurfer in the image and
|
|
10
|
+
``recon-all`` as the default template, and no engine code of its own.
|
|
11
|
+
|
|
12
|
+
Key components:
|
|
13
|
+
- plugin_build: the factory
|
|
14
|
+
- synopsis_build: the manual text for a spec
|
|
15
|
+
- PLUGIN_SIGNATURE: the signature chris_plugin inspects
|
|
16
|
+
|
|
17
|
+
Typical usage:
|
|
18
|
+
PLUGIN: Plugin = plugin_build(PluginSpec(
|
|
19
|
+
name="freesurfer", version=__version__, title="FreeSurfer recon-all",
|
|
20
|
+
defaults={"exec": RECON_ALL, "analyzeFileIndex": "f", "fileFilter": "glob:*.nii.gz"},
|
|
21
|
+
minMemory="8Gi", minCpu="4000m",
|
|
22
|
+
))
|
|
23
|
+
main = PLUGIN.main
|
|
24
|
+
|
|
25
|
+
Dependencies:
|
|
26
|
+
- chris_plugin, pflog, pfdo_run.__main__, pftree.manual, shexec.shexec, shexec.models
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import argparse
|
|
32
|
+
import inspect
|
|
33
|
+
from collections.abc import Callable
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
from chris_plugin import chris_plugin
|
|
37
|
+
from pfdo_run.__main__ import ARGS_SYNOPSIS_DS, CLI_DS, TAG_HELP, parser_build
|
|
38
|
+
from pflog import timed
|
|
39
|
+
from pftree.manual import Manual, manual_print
|
|
40
|
+
|
|
41
|
+
from shexec.models import Plugin, PluginSpec
|
|
42
|
+
from shexec.shexec import plugin_run
|
|
43
|
+
|
|
44
|
+
PLUGIN_SIGNATURE: inspect.Signature = inspect.Signature(
|
|
45
|
+
[
|
|
46
|
+
inspect.Parameter("options", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=argparse.Namespace),
|
|
47
|
+
inspect.Parameter("inputdir", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Path),
|
|
48
|
+
inspect.Parameter("outputdir", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Path),
|
|
49
|
+
],
|
|
50
|
+
return_annotation=int,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def synopsis_build(spec: PluginSpec, shortOnly: bool = False) -> str:
|
|
55
|
+
"""The manual text for a plugin: its NAME, SYNOPSIS and brief example, then the shared sections.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
spec: the plugin.
|
|
59
|
+
shortOnly: NAME, SYNOPSIS and the brief example only.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The text, without the banner.
|
|
63
|
+
"""
|
|
64
|
+
shortSynopsis: str = (
|
|
65
|
+
f"""
|
|
66
|
+
NAME
|
|
67
|
+
|
|
68
|
+
{spec.name}
|
|
69
|
+
|
|
70
|
+
SYNOPSIS
|
|
71
|
+
|
|
72
|
+
{spec.name} \\"""
|
|
73
|
+
+ CLI_DS
|
|
74
|
+
+ "\n\n BRIEF EXAMPLE\n"
|
|
75
|
+
+ spec.briefExample
|
|
76
|
+
)
|
|
77
|
+
if shortOnly:
|
|
78
|
+
return shortSynopsis
|
|
79
|
+
return shortSynopsis + spec.description + "\n ARGS " + ARGS_SYNOPSIS_DS + TAG_HELP + spec.examples
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def manual_build(spec: PluginSpec) -> Manual:
|
|
83
|
+
"""The manual page for a plugin."""
|
|
84
|
+
return Manual(
|
|
85
|
+
command=spec.name,
|
|
86
|
+
art=spec.bannerArt,
|
|
87
|
+
title=spec.bannerTitle,
|
|
88
|
+
text=spec.bannerText,
|
|
89
|
+
version=spec.version,
|
|
90
|
+
synopsisShort=synopsis_build(spec, shortOnly=True),
|
|
91
|
+
synopsisFull=synopsis_build(spec, shortOnly=False),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def parser_forSpec(spec: PluginSpec) -> argparse.ArgumentParser:
|
|
96
|
+
"""pfdo_run's parser named for the plugin, its defaults set from the spec.
|
|
97
|
+
|
|
98
|
+
The defaults are set on the parser rather than applied afterwards, so the
|
|
99
|
+
description ``chris_plugin_info`` serialises carries them and ChRIS shows them.
|
|
100
|
+
"""
|
|
101
|
+
parser: argparse.ArgumentParser = parser_build(standalone=False, command=spec.name, version=spec.version)
|
|
102
|
+
parser.set_defaults(**spec.defaults)
|
|
103
|
+
return parser
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def plugin_build(spec: PluginSpec, singleton: bool = True) -> Plugin:
|
|
107
|
+
"""Make a ChRIS plugin from a spec.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
spec: name, version, description for ChRIS, flag defaults, manual text.
|
|
111
|
+
singleton: register with chris_plugin's registry, which ``chris_plugin_info``
|
|
112
|
+
reads; a test building several plugins in one process passes False.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
The plugin: its parser, its ``main`` for the console script, and its manual.
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
>>> plugin = plugin_build(PluginSpec(name="x", version="0.1"), singleton=False)
|
|
119
|
+
>>> plugin.name, plugin.parser.get_default("exec")
|
|
120
|
+
('x', '')
|
|
121
|
+
"""
|
|
122
|
+
parser: argparse.ArgumentParser = parser_forSpec(spec)
|
|
123
|
+
manual: Manual = manual_build(spec)
|
|
124
|
+
|
|
125
|
+
def body(options: argparse.Namespace, inputdir: Path, outputdir: Path) -> int:
|
|
126
|
+
if options.man or options.synopsis:
|
|
127
|
+
manual_print(manual, shortOnly=not options.man)
|
|
128
|
+
return 0
|
|
129
|
+
return plugin_run(options, inputdir, outputdir)
|
|
130
|
+
|
|
131
|
+
body.__name__ = spec.name
|
|
132
|
+
body.__qualname__ = spec.name
|
|
133
|
+
body.__signature__ = PLUGIN_SIGNATURE # type: ignore[attr-defined] # what chris_plugin inspects; this module stringifies annotations
|
|
134
|
+
timedBody: Callable[[argparse.Namespace, Path, Path], int] = timed(spec.name)(body)
|
|
135
|
+
main: Callable[..., int] = chris_plugin(
|
|
136
|
+
parser=parser,
|
|
137
|
+
title=spec.title,
|
|
138
|
+
category=spec.category,
|
|
139
|
+
min_memory_limit=spec.minMemory,
|
|
140
|
+
min_cpu_limit=spec.minCpu,
|
|
141
|
+
min_gpu_limit=spec.minGpu,
|
|
142
|
+
singleton=singleton,
|
|
143
|
+
)(timedBody)
|
|
144
|
+
return Plugin(name=spec.name, spec=spec, parser=parser, main=main, manual=manual)
|
shexec/shexec.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""What the plugin does: pfdo_run over the directories ChRIS hands it.
|
|
2
|
+
|
|
3
|
+
A ChRIS DS plugin receives its input and output directories as positional arguments
|
|
4
|
+
and everything else as flags. ``ShExec`` is pfdo_run's engine with its options built
|
|
5
|
+
from those three things, and ``plugin_run`` is the body of the plugin's ``main``:
|
|
6
|
+
configure logging, run, report, and return an exit status.
|
|
7
|
+
|
|
8
|
+
Key components:
|
|
9
|
+
- ShExec: pfdo_run's engine, built from the plugin's arguments
|
|
10
|
+
- options_build: the plugin's arguments as ShExecOptions
|
|
11
|
+
- plugin_run: run and report, returning the exit status
|
|
12
|
+
|
|
13
|
+
Typical usage:
|
|
14
|
+
return plugin_run(options, inputdir, outputdir)
|
|
15
|
+
|
|
16
|
+
Dependencies:
|
|
17
|
+
- pfdo_run, pftree.__main__ (logging), shexec.models
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from pfdo_run.pfdo_run import PfdoRun
|
|
28
|
+
from pftree.__main__ import logging_configure
|
|
29
|
+
|
|
30
|
+
from shexec.models import RunResult, ShExecOptions
|
|
31
|
+
|
|
32
|
+
EXIT_OK: int = 0
|
|
33
|
+
EXIT_FAILED: int = 1
|
|
34
|
+
EXIT_USAGE: int = 2
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def options_build(options: argparse.Namespace, inputdir: Path, outputdir: Path) -> ShExecOptions:
|
|
38
|
+
"""The plugin's arguments as options.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
options: the parsed flags.
|
|
42
|
+
inputdir: the positional input directory.
|
|
43
|
+
outputdir: the positional output directory.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
pfdo_run's options with the two directories set.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: a flag's value cannot be coerced to its option's type.
|
|
50
|
+
"""
|
|
51
|
+
values: dict[str, object] = dict(vars(options))
|
|
52
|
+
values["inputDir"] = inputdir
|
|
53
|
+
values["outputDir"] = outputdir
|
|
54
|
+
return ShExecOptions.fromDict(values)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ShExec(PfdoRun):
|
|
58
|
+
"""pfdo_run as a ChRIS plugin runs it.
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
>>> ShExec(ShExecOptions(inputDir=Path("."), outputDir=Path("/tmp/o"), exec="true")).argvTemplate
|
|
62
|
+
['true']
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def fromPlugin(cls, options: argparse.Namespace, inputdir: Path, outputdir: Path) -> ShExec:
|
|
67
|
+
"""The engine, built from what the plugin was given."""
|
|
68
|
+
return cls(options_build(options, inputdir, outputdir))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def result_report(options: argparse.Namespace, result: RunResult) -> None:
|
|
72
|
+
"""Print what the flags asked for: the record as JSON, the elapsed time."""
|
|
73
|
+
if options.json:
|
|
74
|
+
print(json.dumps(result.toDict(), indent=4, sort_keys=True))
|
|
75
|
+
if options.printElapsedTime:
|
|
76
|
+
print(f"Elapsed time = {result.runTime:.6f} seconds")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def plugin_run(options: argparse.Namespace, inputdir: Path, outputdir: Path) -> int:
|
|
80
|
+
"""The body of the plugin's main.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
options: the parsed flags; ``exec`` must be set.
|
|
84
|
+
inputdir: the input tree.
|
|
85
|
+
outputdir: the output tree.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
0 when at least one directory's jobs all succeeded (pftree's stage rule);
|
|
89
|
+
1 when the run failed everywhere or could not start; 2 when ``--exec`` is
|
|
90
|
+
empty or a flag's value is unusable.
|
|
91
|
+
"""
|
|
92
|
+
if not options.exec:
|
|
93
|
+
print("shexec: --exec is required; see shexec --man", file=sys.stderr)
|
|
94
|
+
return EXIT_USAGE
|
|
95
|
+
try:
|
|
96
|
+
engine: ShExec = ShExec.fromPlugin(options, inputdir, outputdir)
|
|
97
|
+
except ValueError as err:
|
|
98
|
+
print(f"shexec: {err}", file=sys.stderr)
|
|
99
|
+
return EXIT_USAGE
|
|
100
|
+
logging_configure(engine.options.verbosity)
|
|
101
|
+
result: RunResult = engine.run()
|
|
102
|
+
result_report(options, result)
|
|
103
|
+
if result.error:
|
|
104
|
+
print(result.error, file=sys.stderr)
|
|
105
|
+
return EXIT_OK if result.status else EXIT_FAILED
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shexec
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: Run a shell-type command over every file of an input tree: a ChRIS DS plugin on pfdo_run
|
|
5
|
+
Home-page: https://github.com/FNNDSC/pl-shexec
|
|
6
|
+
Author: FNNDSC
|
|
7
|
+
Author-email: dev@babyMRI.org
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/FNNDSC/pl-shexec
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: POSIX
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/x-rst
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: chris_plugin>=0.4.0
|
|
21
|
+
Requires-Dist: pfdo>=6.2.0
|
|
22
|
+
Requires-Dist: pfdo_run>=4.2.0
|
|
23
|
+
Requires-Dist: hostlib>=1.2.0
|
|
24
|
+
Requires-Dist: pflog>=2.0.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
pl-shexec
|
|
31
|
+
=========
|
|
32
|
+
|
|
33
|
+
**Run a command over every file of a tree, as a ChRIS plugin; and the pattern for turning any command-line program into one.**
|
|
34
|
+
|
|
35
|
+
``pl-shexec`` is a `ChRIS <https://chrisproject.org/>`_ DS plugin that does two things. In a pipeline it is the plugin for the jobs too small to deserve one of their own: convert every image, zip every directory, rename, copy one file out of each subject, count lines. It takes a command template, runs it once per selected file of every directory in its input tree, and writes the results, and a record of each run, into an output tree that mirrors the input.
|
|
36
|
+
|
|
37
|
+
.. code-block:: bash
|
|
38
|
+
|
|
39
|
+
shexec --fileFilter 'glob:*.jpg' \
|
|
40
|
+
--exec "convert %{inputWorkingDir}/%{inputWorkingFile}
|
|
41
|
+
%{outputWorkingDir}/%{inputWorkingFile|rmext}.png" \
|
|
42
|
+
/incoming /outgoing
|
|
43
|
+
|
|
44
|
+
It is also the design for wrapping a large existing program as a plugin. Put the program in the image, set the default command, and the plugin is done: the program's whole command line stays available through the template rather than a hand-picked subset of flags, and the tree walk, the records, the parallelism and the exit status come from the stack. With for example FreeSurfer in the image, one ``recon-all`` per subject directory is:
|
|
45
|
+
|
|
46
|
+
.. code-block:: bash
|
|
47
|
+
|
|
48
|
+
shexec --analyzeFileIndex f --fileFilter 'glob:*.nii.gz' \
|
|
49
|
+
--exec "recon-all -all -i %{inputWorkingDir}/%{inputWorkingFile}
|
|
50
|
+
-s %{inputWorkingDir|basename} -sd %{outputWorkingDir}" \
|
|
51
|
+
/incoming /outgoing
|
|
52
|
+
|
|
53
|
+
pl-freesurfer is an example of the pattern: a plugin that depends on this one the way this one depends on pfdo_run, contributing the FreeSurfer image, that template as the default ``--exec``, and nothing else. See `Building a plugin on pl-shexec`_.
|
|
54
|
+
|
|
55
|
+
The engine is `pfdo_run <https://github.com/FNNDSC/pfdo_run>`_; the plugin is pfdo_run with its directories supplied by ChRIS and its own run timed.
|
|
56
|
+
|
|
57
|
+
The template
|
|
58
|
+
------------
|
|
59
|
+
|
|
60
|
+
``--exec`` is not a shell command. It is a template whose ``%{...}`` tags name the file and directories in hand, resolved once per file, and run directly without a shell, so a path with a space in it stays one argument and nothing is interpreted that was not written. Three tags describe the job:
|
|
61
|
+
|
|
62
|
+
========================== ==============================================
|
|
63
|
+
``%{inputWorkingDir}`` the input directory being processed
|
|
64
|
+
``%{outputWorkingDir}`` its counterpart in the output tree
|
|
65
|
+
``%{inputWorkingFile}`` the file being processed
|
|
66
|
+
========================== ==============================================
|
|
67
|
+
|
|
68
|
+
Functions derive one name from another: ``%{inputWorkingFile|rmext}.png`` turns ``scan.jpg`` into ``scan.png``; ``%{inputWorkingDir|basename}`` is the directory's own name. Every flag of the plugin is a tag too, and so are ``%{timestamp}``, ``%{hostname}`` and ``%{user}``. The grammar is `pftag <https://github.com/FNNDSC/pftag>`_'s; ``shexec --man`` lists it under TAGS. A template with no tag draws a warning, since the same command would then run for every file.
|
|
69
|
+
|
|
70
|
+
What each run leaves behind
|
|
71
|
+
---------------------------
|
|
72
|
+
|
|
73
|
+
Each directory of the output tree gets, beside its results, one file, ``jobs.jsonl``, with one line per job that ran there:
|
|
74
|
+
|
|
75
|
+
.. code-block:: text
|
|
76
|
+
|
|
77
|
+
scan.png
|
|
78
|
+
jobs.jsonl
|
|
79
|
+
|
|
80
|
+
The line is the whole record of the job as JSON: the command as executed, everything it printed and everything it complained, its exit status, the host it ran on, and when it started and ended. ``pflog read out/**/jobs.jsonl`` prints a day's jobs as a table, ``--failed`` keeps the ones that went wrong, and a log collector indexes the lines by field.
|
|
81
|
+
|
|
82
|
+
.. code-block:: text
|
|
83
|
+
|
|
84
|
+
STARTED DURATION STATUS NAME
|
|
85
|
+
2026-10-14T16:02:07-04:00 4.213 ok convert /incoming/a/scan.jpg /outgoing/a/scan.png
|
|
86
|
+
2026-10-14T16:02:12-04:00 0.004 failed convert /incoming/b/bad.jpg /outgoing/b/bad.png
|
|
87
|
+
|
|
88
|
+
``--jobFiles`` also writes each job as four files beside its result, ``scan.jpg-cmd``, ``-stdout``, ``-stderr`` and ``-returncode``, the stream files rendered as a terminal would have shown them, for the reader who opens them in an editor. ``--noJobLogging`` keeps no record at all. ``--json`` prints the whole run's record on standard output.
|
|
89
|
+
|
|
90
|
+
The plugin's own run is timed by `pflog <https://github.com/FNNDSC/pflog>`_: one line of JSON on standard error, when it began, when it ended, the seconds, and whether it succeeded. CUBE keeps that line with the rest of the plugin's output, and any log collector indexes it.
|
|
91
|
+
|
|
92
|
+
Selecting files and directories
|
|
93
|
+
-------------------------------
|
|
94
|
+
|
|
95
|
+
The filters are pftree's. ``--fileFilter`` and ``--dirFilter`` take substrings, or ``glob:`` and ``re:`` patterns, joined by ``--fileFilterLogic`` and ``--dirFilterLogic``; ``--maxDepth`` bounds the descent. ``--analyzeFileIndex`` chooses which of a directory's selected files get the command: ``a`` for all, ``f`` for the first, ``l`` for the last, ``m`` for the middle, or an index. ``f`` is how one command runs per directory, started on the first file, as for a DICOM series.
|
|
96
|
+
|
|
97
|
+
Directories run in parallel under ``--threads``. At ``--verbosity 2`` each command's output streams to the console as it runs.
|
|
98
|
+
|
|
99
|
+
Exit status
|
|
100
|
+
-----------
|
|
101
|
+
|
|
102
|
+
0 when at least one directory's jobs all succeeded; 1 when every directory failed or the run could not start; 2 when ``--exec`` is missing or a flag's value is unusable. A failed template, a command that cannot be found, or a non-zero exit fails its directory and the run continues; the record says why.
|
|
103
|
+
|
|
104
|
+
Wrapping a program
|
|
105
|
+
------------------
|
|
106
|
+
|
|
107
|
+
A plugin for a program with its own command line, FreeSurfer, ANTs, FSL, DCMTK, is pl-shexec with three changes and no new code:
|
|
108
|
+
|
|
109
|
+
- the image carries the program, with whatever licence or data it needs;
|
|
110
|
+
- the default ``--exec`` is the program's usual invocation as a template, so the common case needs no command at all and the unusual case overrides it;
|
|
111
|
+
- the default ``--analyzeFileIndex`` chooses the unit, ``f`` for one run per directory, as for a subject or a DICOM series, ``a`` for one run per file.
|
|
112
|
+
|
|
113
|
+
The user of such a plugin gets the program's entire interface, because anything the program accepts can be written into the template, and gets the same records, the same filters, the same threads and the same truthful exit status as every other plugin on the stack. This is the reason pl-shexec exists in the form it has.
|
|
114
|
+
|
|
115
|
+
Building a plugin on pl-shexec
|
|
116
|
+
------------------------------
|
|
117
|
+
|
|
118
|
+
pl-shexec is a library as well as a plugin, in the same way pfdo_run is a library to pl-shexec. A downstream plugin does not copy it; it depends on the ``shexec`` package and calls ``plugin_build`` with a spec of what differs:
|
|
119
|
+
|
|
120
|
+
.. code-block:: python
|
|
121
|
+
|
|
122
|
+
# freesurfer/__main__.py, the whole of pl-freesurfer's code
|
|
123
|
+
from shexec import PluginSpec, plugin_build
|
|
124
|
+
|
|
125
|
+
from freesurfer import __version__
|
|
126
|
+
|
|
127
|
+
RECON_ALL: str = (
|
|
128
|
+
"recon-all -all -i %{inputWorkingDir}/%{inputWorkingFile} "
|
|
129
|
+
"-s %{inputWorkingDir|basename} -sd %{outputWorkingDir}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
PLUGIN = plugin_build(PluginSpec(
|
|
133
|
+
name="freesurfer",
|
|
134
|
+
version=__version__,
|
|
135
|
+
title="FreeSurfer recon-all over every subject",
|
|
136
|
+
minMemory="8Gi",
|
|
137
|
+
minCpu="4000m",
|
|
138
|
+
defaults={"exec": RECON_ALL, "analyzeFileIndex": "f", "fileFilter": "glob:*.nii.gz"},
|
|
139
|
+
bannerTitle="FreeSurfer recon-all over an input tree",
|
|
140
|
+
description=DESCRIPTION,
|
|
141
|
+
examples=EXAMPLES,
|
|
142
|
+
))
|
|
143
|
+
main = PLUGIN.main
|
|
144
|
+
|
|
145
|
+
``PluginSpec`` holds everything a plugin changes: its name and version, which become its command, its span and its ``--version``; its title, category and resource limits for ChRIS; the defaults of its flags, above all ``--exec``, which ChRIS shows as the defaults and the user can override; and its words on the manual page, over the sections every plugin shares. ``plugin_build`` returns the parser with those defaults applied, the manual, and ``main`` already wrapped in pflog's ``timed`` and ``chris_plugin``, ready to be the console script. The engine, the flags, the records and the exit status are not in the spec, because they are the same for every plugin on the stack, and a fix to them reaches every plugin on its next build.
|
|
146
|
+
|
|
147
|
+
The rest of pl-freesurfer is a ``pyproject.toml`` naming ``chris_plugin`` and ``shexec`` as its dependencies, the first so that ``chris_plugin_info`` finds the plugin, a ``setup.cfg``, a Dockerfile that starts from the FreeSurfer image and installs the package, tests, and docs. pl-shexec itself is the first plugin built this way; its ``__main__`` is the same call with an empty default command. The pf* stack's ``docs/plugin.adoc`` has the template.
|
|
148
|
+
|
|
149
|
+
Running it
|
|
150
|
+
----------
|
|
151
|
+
|
|
152
|
+
From ChRIS, add the plugin to a pipeline and give it ``--exec`` and filters as parameters.
|
|
153
|
+
|
|
154
|
+
From the command line, with the image:
|
|
155
|
+
|
|
156
|
+
.. code-block:: bash
|
|
157
|
+
|
|
158
|
+
docker run --rm -v $PWD/in:/incoming:ro -v $PWD/out:/outgoing:rw \
|
|
159
|
+
fnndsc/pl-shexec shexec --fileFilter txt \
|
|
160
|
+
--exec "wc -l %{inputWorkingDir}/%{inputWorkingFile}" /incoming /outgoing
|
|
161
|
+
|
|
162
|
+
docker run --rm fnndsc/pl-shexec shexec --man /tmp /tmp
|
|
163
|
+
|
|
164
|
+
With Apptainer, ``apptainer exec docker://fnndsc/pl-shexec shexec ...``. The image carries the coreutils, zip and unzip, bc, perl, tar, ImageMagick and the DCMTK tools.
|
|
165
|
+
|
|
166
|
+
As a Python package, ``pip install .`` in this repository gives the ``shexec`` command and the ``shexec`` module, whose ``plugin_run(options, inputdir, outputdir)`` is the body of the plugin and whose ``shexec.models`` re-exports every record pfdo_run, pfdo, pftree, hostlib and pflog produce.
|
|
167
|
+
|
|
168
|
+
Development
|
|
169
|
+
-----------
|
|
170
|
+
|
|
171
|
+
.. code-block:: bash
|
|
172
|
+
|
|
173
|
+
uv venv --python /usr/bin/python3.14 .venv
|
|
174
|
+
uv pip install --python .venv/bin/python -e ".[dev]"
|
|
175
|
+
.venv/bin/ruff check . && .venv/bin/pytest -q
|
|
176
|
+
PATH=$PWD/.venv/bin:$PATH chris_plugin_info --dock-image docker.io/fnndsc/pl-shexec:2.1.0 shexec
|
|
177
|
+
|
|
178
|
+
The CI workflow lints and tests on Python 3.11 to 3.14, builds the image on every push to ``main``, pushes it to Docker Hub and ghcr.io, and on a ``v*`` tag uploads the plugin's description to cube.chrisproject.org and publishes the ``shexec`` package to PyPI, so that a plugin built on pl-shexec can depend on it. The tag must match ``__version__`` in ``shexec/__init__.py``.
|
|
179
|
+
|
|
180
|
+
Layout
|
|
181
|
+
------
|
|
182
|
+
|
|
183
|
+
.. code-block:: text
|
|
184
|
+
|
|
185
|
+
shexec/
|
|
186
|
+
├── __main__.py the plugin: parser, manual, @chris_plugin @timed main
|
|
187
|
+
├── shexec.py ShExec, options_build, plugin_run
|
|
188
|
+
└── models.py ShExecOptions, and every record below re-exported
|
|
189
|
+
|
|
190
|
+
``docs/purpose.adoc`` says why the plugin exists, ``docs/architecture.adoc`` how it is built, ``docs/history.adoc`` where it came from.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
shexec/__init__.py,sha256=JBWyItKMRZc6meHZAr8whp1rgtP5uYRoMr-8JOKoZ0M,1142
|
|
2
|
+
shexec/__main__.py,sha256=7PRKuS0p5mEesYwcCdnot5vF_SiTAPKPuMWTt30hsuw,5943
|
|
3
|
+
shexec/models.py,sha256=WXpSzJunootUAOyUDb0MLhmsxiKhHQp3CWNZH272wz4,4361
|
|
4
|
+
shexec/plugin.py,sha256=yveDL-ZfAE8VpRNKqt5gCRylmM_KgVnigqiFu_MVo5M,5353
|
|
5
|
+
shexec/shexec.py,sha256=Uh8Y30Y3lA9S1Tp_UpyG9tGd5KqhKQPShP4_t4DBwSU,3497
|
|
6
|
+
shexec-2.2.0.dist-info/licenses/LICENSE,sha256=X3Pi8IcuV0-qXGRNP4KNpb7OIorb1xqK9dyfaVUJOEc,1069
|
|
7
|
+
shexec-2.2.0.dist-info/METADATA,sha256=EUI8ykdMXYKUHQff7js0NRTgiYQIYu4sYflI_LEFwPs,11822
|
|
8
|
+
shexec-2.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
9
|
+
shexec-2.2.0.dist-info/entry_points.txt,sha256=5IpO-OMKqwNt0icC2XOShqP1hYeHe_wUKQ1weNhP3sk,48
|
|
10
|
+
shexec-2.2.0.dist-info/top_level.txt,sha256=Hx8DpB-sDGRc7bjHLX5XBkL69kOZPLPK3IYtufJ5Px4,7
|
|
11
|
+
shexec-2.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 FNNDSC / BCH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shexec
|