pfdo-run 3.2.6__tar.gz → 4.0.0__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.
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: pfdo_run
3
+ Version: 4.0.0
4
+ Summary: Run a command, written as a pftag template, in every directory of a tree
5
+ Author-email: FNNDSC <dev@babyMRI.org>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/FNNDSC/pfdo_run
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/x-rst
17
+ License-File: LICENSE
18
+ Requires-Dist: pfdo>=6.1.0
19
+ Requires-Dist: pftag>=3.1.0
20
+ Requires-Dist: hostlib>=1.1.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8; extra == "dev"
23
+ Requires-Dist: ruff>=0.6; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ pfdo_run
27
+ ========
28
+
29
+ .. image:: https://github.com/FNNDSC/pfdo_run/actions/workflows/ci.yml/badge.svg
30
+ :target: https://github.com/FNNDSC/pfdo_run/actions/workflows/ci.yml
31
+
32
+ .. image:: https://img.shields.io/pypi/v/pfdo_run.svg
33
+ :target: https://pypi.org/project/pfdo_run/
34
+
35
+ .. image:: https://img.shields.io/badge/python-3.11%2B-blue.svg
36
+ :target: https://pypi.org/project/pfdo_run/
37
+
38
+ Run a command, written as a pftag template, in every directory of a tree.
39
+
40
+ .. contents:: Table of Contents
41
+
42
+ Overview
43
+ --------
44
+
45
+ ``pfdo_run`` walks an input directory with ``pftree``, selects files in each directory with the ``pfdo`` filters and ``--analyzeFileIndex``, and runs the ``--exec`` command once per selected file. The command is a ``pftag`` template: ``%{inputWorkingDir}``, ``%{outputWorkingDir}`` and ``%{inputWorkingFile}`` name the directory being processed, its counterpart in the output tree, and the file; functions derive names, ``%{inputWorkingFile|rmext}.png``. The output tree mirrors the input tree, and each job leaves its command, stdout, stderr and return code beside its results. Directories run in parallel under ``--threads``.
46
+
47
+ The template is split like a shell command line and each argument has its tags resolved on its own, so a path with a space stays one argument and nothing a file name contains can change the command. The command runs without a shell.
48
+
49
+ Installation
50
+ ------------
51
+
52
+ .. code:: bash
53
+
54
+ pip install pfdo_run
55
+
56
+ Python 3.11 or newer; depends on ``pfdo`` and ``pftag``.
57
+
58
+ Command line
59
+ ------------
60
+
61
+ .. code:: bash
62
+
63
+ pfdo_run --inputDir /data --outputDir /png --fileFilter 'glob:*.jpg' --threads 4 \
64
+ --exec "convert %{inputWorkingDir}/%{inputWorkingFile} %{outputWorkingDir}/%{inputWorkingFile|rmext}.png"
65
+
66
+ pfdo_run --inputDir /dicom --outputDir /nifti --fileFilter 'glob:*.dcm' --analyzeFileIndex f \
67
+ --exec "mri_convert %{inputWorkingDir}/%{inputWorkingFile} %{outputWorkingDir}/%{inputWorkingDir|basename}.nii.gz"
68
+
69
+ pfdo_run --man
70
+
71
+ All of ``pftree``'s and ``pfdo``'s options apply. ``pfdo_run`` adds:
72
+
73
+ ``--exec <commandTemplate>``
74
+ The command to run per selected file. Tags: ``%{inputWorkingDir}``, ``%{outputWorkingDir}``, ``%{inputWorkingFile}``, every option by name (``%{outputDir}``), and pftag's core tags. Functions: ``rmext``, ``ext``, ``basename``, ``dirname``, ``replace``, ``re``, ``upper``, ``lower``, ``strip``, ``pad``, ``slice``, ``md5``, ``sha256``, ``strftime``, ``mask``. A tag that does not resolve fails that job before it runs.
75
+
76
+ ``--noJobLogging``
77
+ Do not write ``<file>-cmd``, ``<file>-stdout``, ``<file>-stderr`` and ``<file>-returncode`` beside each job's results. The stream files are written live and rendered as a terminal would show them when the job ends.
78
+
79
+ A job whose command exits non-zero fails its directory; the run continues and the directory's record says why. ``--json`` prints the whole result, each directory's jobs included.
80
+
81
+ Library
82
+ -------
83
+
84
+ .. code:: python
85
+
86
+ from pathlib import Path
87
+ from pfdo_run import PfdoRun, PfdoRunOptions
88
+
89
+ options = PfdoRunOptions(
90
+ inputDir=Path("/data"), outputDir=Path("/png"), fileFilter="glob:*.jpg", threads=4,
91
+ exec="convert %{inputWorkingDir}/%{inputWorkingFile} %{outputWorkingDir}/%{inputWorkingFile|rmext}.png",
92
+ )
93
+ result = PfdoRun(options).run()
94
+ for key, stage in result.process.analyze.items():
95
+ for job in stage.payload["jobs"]:
96
+ print(key, job["file"], job["run"]["returncode"] if job["run"] else job["errors"])
97
+
98
+ ``pfdo_run.__main__`` exposes its parser and manual in parts (``parser_build(standalone=False)``, ``synopsis(command=...)``, ``CLI_DS``, ``ARGS_SYNOPSIS_DS``) so that a ChRIS plugin such as ``pl-shexec`` composes its own command around them.
99
+
100
+ Changes in 4.0
101
+ --------------
102
+
103
+ - Built on ``pfdo`` 6 and ``pftag`` 3; ``pfmisc``, ``pudb`` and ``Faker`` are no longer dependencies.
104
+ - The template grammar is pftag's: ``%{name}`` and ``%{name|fn(arg)}``. The 3.x forms ``%name`` and ``%_fn_name`` are gone; a bare ``%name`` of a known tag is reported as a 1.x template and fails the job.
105
+ - The command is split before substitution and run without a shell; 3.x substituted into the string and split on whitespace, so a path with a space broke it.
106
+ - The command runs through ``hostlib``: no shell, both streams read concurrently as they arrive, so nothing is dropped and nothing can deadlock; 3.x read stdout line by line, lost the last line, and could deadlock on stderr. At ``--verbosity 2`` and above the output also streams to the console while the job runs.
107
+ - Per-job records are written by default and ``--noJobLogging`` exists; in 3.x the option was documented but undefined, so logs were never written. The stream files are rendered as a terminal would have shown them, so a redrawn progress bar is one line; the record in ``--json`` keeps every frame.
108
+ - Each job's record is ``FileJob``: the file, the command as built, status, errors, and ``run``, hostlib's envelope of the run (both streams, exit status, host facts, timing) when the command ran.
109
+ - A non-zero exit status fails the job and its directory; the run's status is truthful (3.x reported False always).
110
+ - ``main(argv)`` honours ``argv``; ``--version``, ``--man`` and ``--synopsis`` exit 0; packaging is ``pyproject.toml`` only, published from CI on a ``v*`` tag.
@@ -0,0 +1,85 @@
1
+ pfdo_run
2
+ ========
3
+
4
+ .. image:: https://github.com/FNNDSC/pfdo_run/actions/workflows/ci.yml/badge.svg
5
+ :target: https://github.com/FNNDSC/pfdo_run/actions/workflows/ci.yml
6
+
7
+ .. image:: https://img.shields.io/pypi/v/pfdo_run.svg
8
+ :target: https://pypi.org/project/pfdo_run/
9
+
10
+ .. image:: https://img.shields.io/badge/python-3.11%2B-blue.svg
11
+ :target: https://pypi.org/project/pfdo_run/
12
+
13
+ Run a command, written as a pftag template, in every directory of a tree.
14
+
15
+ .. contents:: Table of Contents
16
+
17
+ Overview
18
+ --------
19
+
20
+ ``pfdo_run`` walks an input directory with ``pftree``, selects files in each directory with the ``pfdo`` filters and ``--analyzeFileIndex``, and runs the ``--exec`` command once per selected file. The command is a ``pftag`` template: ``%{inputWorkingDir}``, ``%{outputWorkingDir}`` and ``%{inputWorkingFile}`` name the directory being processed, its counterpart in the output tree, and the file; functions derive names, ``%{inputWorkingFile|rmext}.png``. The output tree mirrors the input tree, and each job leaves its command, stdout, stderr and return code beside its results. Directories run in parallel under ``--threads``.
21
+
22
+ The template is split like a shell command line and each argument has its tags resolved on its own, so a path with a space stays one argument and nothing a file name contains can change the command. The command runs without a shell.
23
+
24
+ Installation
25
+ ------------
26
+
27
+ .. code:: bash
28
+
29
+ pip install pfdo_run
30
+
31
+ Python 3.11 or newer; depends on ``pfdo`` and ``pftag``.
32
+
33
+ Command line
34
+ ------------
35
+
36
+ .. code:: bash
37
+
38
+ pfdo_run --inputDir /data --outputDir /png --fileFilter 'glob:*.jpg' --threads 4 \
39
+ --exec "convert %{inputWorkingDir}/%{inputWorkingFile} %{outputWorkingDir}/%{inputWorkingFile|rmext}.png"
40
+
41
+ pfdo_run --inputDir /dicom --outputDir /nifti --fileFilter 'glob:*.dcm' --analyzeFileIndex f \
42
+ --exec "mri_convert %{inputWorkingDir}/%{inputWorkingFile} %{outputWorkingDir}/%{inputWorkingDir|basename}.nii.gz"
43
+
44
+ pfdo_run --man
45
+
46
+ All of ``pftree``'s and ``pfdo``'s options apply. ``pfdo_run`` adds:
47
+
48
+ ``--exec <commandTemplate>``
49
+ The command to run per selected file. Tags: ``%{inputWorkingDir}``, ``%{outputWorkingDir}``, ``%{inputWorkingFile}``, every option by name (``%{outputDir}``), and pftag's core tags. Functions: ``rmext``, ``ext``, ``basename``, ``dirname``, ``replace``, ``re``, ``upper``, ``lower``, ``strip``, ``pad``, ``slice``, ``md5``, ``sha256``, ``strftime``, ``mask``. A tag that does not resolve fails that job before it runs.
50
+
51
+ ``--noJobLogging``
52
+ Do not write ``<file>-cmd``, ``<file>-stdout``, ``<file>-stderr`` and ``<file>-returncode`` beside each job's results. The stream files are written live and rendered as a terminal would show them when the job ends.
53
+
54
+ A job whose command exits non-zero fails its directory; the run continues and the directory's record says why. ``--json`` prints the whole result, each directory's jobs included.
55
+
56
+ Library
57
+ -------
58
+
59
+ .. code:: python
60
+
61
+ from pathlib import Path
62
+ from pfdo_run import PfdoRun, PfdoRunOptions
63
+
64
+ options = PfdoRunOptions(
65
+ inputDir=Path("/data"), outputDir=Path("/png"), fileFilter="glob:*.jpg", threads=4,
66
+ exec="convert %{inputWorkingDir}/%{inputWorkingFile} %{outputWorkingDir}/%{inputWorkingFile|rmext}.png",
67
+ )
68
+ result = PfdoRun(options).run()
69
+ for key, stage in result.process.analyze.items():
70
+ for job in stage.payload["jobs"]:
71
+ print(key, job["file"], job["run"]["returncode"] if job["run"] else job["errors"])
72
+
73
+ ``pfdo_run.__main__`` exposes its parser and manual in parts (``parser_build(standalone=False)``, ``synopsis(command=...)``, ``CLI_DS``, ``ARGS_SYNOPSIS_DS``) so that a ChRIS plugin such as ``pl-shexec`` composes its own command around them.
74
+
75
+ Changes in 4.0
76
+ --------------
77
+
78
+ - Built on ``pfdo`` 6 and ``pftag`` 3; ``pfmisc``, ``pudb`` and ``Faker`` are no longer dependencies.
79
+ - The template grammar is pftag's: ``%{name}`` and ``%{name|fn(arg)}``. The 3.x forms ``%name`` and ``%_fn_name`` are gone; a bare ``%name`` of a known tag is reported as a 1.x template and fails the job.
80
+ - The command is split before substitution and run without a shell; 3.x substituted into the string and split on whitespace, so a path with a space broke it.
81
+ - The command runs through ``hostlib``: no shell, both streams read concurrently as they arrive, so nothing is dropped and nothing can deadlock; 3.x read stdout line by line, lost the last line, and could deadlock on stderr. At ``--verbosity 2`` and above the output also streams to the console while the job runs.
82
+ - Per-job records are written by default and ``--noJobLogging`` exists; in 3.x the option was documented but undefined, so logs were never written. The stream files are rendered as a terminal would have shown them, so a redrawn progress bar is one line; the record in ``--json`` keeps every frame.
83
+ - Each job's record is ``FileJob``: the file, the command as built, status, errors, and ``run``, hostlib's envelope of the run (both streams, exit status, host facts, timing) when the command ran.
84
+ - A non-zero exit status fails the job and its directory; the run's status is truthful (3.x reported False always).
85
+ - ``main(argv)`` honours ``argv``; ``--version``, ``--man`` and ``--synopsis`` exit 0; packaging is ``pyproject.toml`` only, published from CI on a ``v*`` tag.
@@ -0,0 +1,39 @@
1
+ """pfdo_run: run a command in every directory of a tree.
2
+
3
+ The member of the pfdo family whose per-directory operation is a command the user
4
+ writes as a pftag template. ``%{inputWorkingDir}``, ``%{outputWorkingDir}`` and
5
+ ``%{inputWorkingFile}`` name the current directory's paths and file; functions such as
6
+ ``%{inputWorkingFile|rmext}`` derive output names.
7
+
8
+ Key components:
9
+ - PfdoRun: the engine
10
+ - PfdoRunOptions, FileJob: options in, job records out
11
+ - __main__: the ``pfdo_run`` command and the parts pl-shexec composes its own from
12
+
13
+ Typical usage:
14
+ pfdo_run --inputDir /data --outputDir /out --fileFilter 'glob:*.dcm' --analyzeFileIndex f \\
15
+ --exec "mri_convert %{inputWorkingDir}/%{inputWorkingFile} %{outputWorkingDir}/T1.nii.gz"
16
+
17
+ Dependencies:
18
+ - pfdo, pftag, hostlib
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ __version__: str = "4.0.0"
24
+
25
+ from pfdo_run.models import DirNode, FileJob, JobResult, PfdoRunOptions, RunResult, StageResult
26
+ from pfdo_run.pfdo_run import PfdoRun, lookups_forJob, sink_build
27
+
28
+ __all__ = [
29
+ "DirNode",
30
+ "FileJob",
31
+ "JobResult",
32
+ "PfdoRun",
33
+ "PfdoRunOptions",
34
+ "RunResult",
35
+ "StageResult",
36
+ "__version__",
37
+ "lookups_forJob",
38
+ "sink_build",
39
+ ]
@@ -0,0 +1,338 @@
1
+ """The ``pfdo_run`` command line and its manual.
2
+
3
+ The parser and the manual are assembled from pftree's and pfdo's parts plus
4
+ pfdo_run's own, and are exposed in parts again for pl-shexec.
5
+
6
+ Key components:
7
+ - CLI_SELF, ARGS_SYNOPSIS_SELF, TAG_HELP, EXAMPLES: pfdo_run's manual pieces
8
+ - CLI_FULL, CLI_DS, ARGS_SYNOPSIS_FULL, ARGS_SYNOPSIS_DS: the assembled pieces
9
+ - synopsis, manual_build: the manual
10
+ - parserSelf_build / parser_build: the parsers
11
+ - main: the entry point
12
+
13
+ Typical usage:
14
+ pfdo_run --inputDir /in --outputDir /out --fileFilter jpg \\
15
+ --exec "convert %{inputWorkingDir}/%{inputWorkingFile} %{outputWorkingDir}/out.png"
16
+ pfdo_run --man
17
+
18
+ Dependencies:
19
+ - pftree, pfdo (their CLI parts and the manual renderer)
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import argparse
25
+ import json
26
+ import sys
27
+ from collections.abc import Sequence
28
+
29
+ from pfdo.__main__ import ARGS_SYNOPSIS_SELF as PFDO_ARGS_SYNOPSIS_SELF
30
+ from pfdo.__main__ import CLI_SELF as PFDO_CLI_SELF
31
+ from pfdo.__main__ import parserSelf_build as pfdoParserSelf_build
32
+ from pftree.__main__ import (
33
+ ARGS_SYNOPSIS_CORE,
34
+ ARGS_SYNOPSIS_DS,
35
+ ARGS_SYNOPSIS_IO,
36
+ CLI_CORE,
37
+ CLI_DS_IO,
38
+ CLI_IO_CORE,
39
+ logging_configure,
40
+ parserCore_build,
41
+ parserIO_build,
42
+ )
43
+ from pftree.manual import Manual, manual_print
44
+
45
+ from pfdo_run import __version__
46
+ from pfdo_run.models import PfdoRunOptions, RunResult
47
+ from pfdo_run.pfdo_run import PfdoRun
48
+
49
+ BANNER_ART: str = r"""
50
+
51
+ __ _
52
+ / _| | |
53
+ _ __ | |_ __| | ___ _ __ _ _ _ __
54
+ | '_ \| _/ _` |/ _ \ | '__| | | | '_ \
55
+ | |_) | || (_| | (_) || | | |_| | | | |
56
+ | .__/|_| \__,_|\___/ |_| \__,_|_| |_|
57
+ | | ______
58
+ |_| |______|
59
+
60
+ """
61
+
62
+ BANNER_TITLE: str = "Path-File Do Run"
63
+
64
+ BANNER_TEXT: str = """
65
+ Path-File Do Run
66
+
67
+ Recursively walk down a directory tree and run a CLI spec'd app
68
+ on files in each directory (optionally filtered by some simple
69
+ expression). Results of each operation are saved in an output tree
70
+ that preserves the input directory structure.
71
+
72
+ -- version {version} --
73
+
74
+ 'pfdo_run' demonstrates how to use pftree to traverse
75
+ directory trees and execute some user specified CLI operation
76
+ at each directory level (that optionally contains files of interest).
77
+
78
+ As part of the "pf*" suite of applications, it is geared to IO as
79
+ directories. Nested directory trees within some input directory
80
+ are reconstructed in an output directory, preserving directory
81
+ structure.
82
+
83
+ """
84
+
85
+ CLI_SELF: str = """
86
+ --exec <commandTemplate> \\
87
+ [--noJobLogging] \\"""
88
+
89
+ ARGS_SYNOPSIS_SELF: str = """
90
+ --exec <commandTemplate>
91
+ The command to run for each selected file of each directory in the
92
+ input tree, written as a pftag template. See the TAGS section. The
93
+ template is split like a shell command line, each argument has its
94
+ tags resolved, and the result is run directly, without a shell: a
95
+ path with a space in it stays one argument, and shell features such
96
+ as pipes and redirection are not available.
97
+
98
+ [--noJobLogging]
99
+ If specified, suppress the per-job record. Usually each job that is
100
+ run leaves, in the output directory, four files:
101
+
102
+ <inputWorkingFile>-cmd
103
+ <inputWorkingFile>-stdout
104
+ <inputWorkingFile>-stderr
105
+ <inputWorkingFile>-returncode
106
+
107
+ The stream files are written as the command runs and, when it ends,
108
+ rendered as a terminal would have shown them: a progress bar redrawn
109
+ a hundred times is one line. By specifying this option, the above
110
+ files are not recorded.
111
+
112
+ At --verbosity 2 and above each command's output is also streamed to
113
+ the console while it runs, so a long job can be watched. Under
114
+ --threads the streams of concurrent jobs interleave.
115
+ """
116
+
117
+ TAG_HELP: str = """
118
+ TAGS
119
+
120
+ The --exec template is resolved by pftag (see pftag --man or the
121
+ pftag docs/tags.adoc). A tag is %{name}, optionally piped through
122
+ functions: %{name|function|function(arg, arg)}. Three tags describe
123
+ the job:
124
+
125
+ %{inputWorkingDir} the input directory being processed
126
+ %{outputWorkingDir} its counterpart in the output tree
127
+ %{inputWorkingFile} the file being processed
128
+
129
+ and every option of pfdo_run is a tag by name, so %{outputDir} is
130
+ the output tree's root. The core tags %{timestamp}, %{date},
131
+ %{hostname}, %{platform}, %{user} and %{pid} are available too.
132
+
133
+ Functions derive one name from another:
134
+
135
+ %{inputWorkingFile|rmext} brain.nii.gz -> brain.nii
136
+ %{inputWorkingFile|rmext|rmext}.jpg brain.nii.gz -> brain.jpg
137
+ %{inputWorkingFile|replace(.,-)} a.b.c -> a-b-c
138
+ %{inputWorkingFile|re([0-9]+,N)} im001.dcm -> imN.dcm
139
+ %{inputWorkingDir|basename} the directory's own name
140
+ %{inputWorkingFile|sha256(8)} a stable hash of the name
141
+
142
+ Nothing outside %{...} is a tag, so a literal % in the command is
143
+ fine. A tag that does not resolve fails that job before it runs;
144
+ the job's record says why.
145
+ """
146
+
147
+ EXAMPLES: str = """
148
+
149
+ EXAMPLES
150
+
151
+ Convert every jpg in a tree to png in a parallel tree:
152
+
153
+ pfdo_run \\
154
+ --inputDir /var/www/html/data --fileFilter 'glob:*.jpg' \\
155
+ --outputDir /var/www/html/png \\
156
+ --exec "convert %{inputWorkingDir}/%{inputWorkingFile}
157
+ %{outputWorkingDir}/%{inputWorkingFile|rmext}.png" \\
158
+ --threads 4 --printElapsedTime
159
+
160
+ Each conversion's command, stdout, stderr and return code are written
161
+ beside the png as <file>-cmd, <file>-stdout, <file>-stderr and
162
+ <file>-returncode. Add --verbosity 2 to watch the conversions as they run.
163
+
164
+ Run one command per DICOM series, started on the first file:
165
+
166
+ pfdo_run \\
167
+ --inputDir /data --outputDir /out \\
168
+ --fileFilter 'glob:*.dcm' --analyzeFileIndex f \\
169
+ --exec "mri_convert %{inputWorkingDir}/%{inputWorkingFile}
170
+ %{outputWorkingDir}/%{inputWorkingDir|basename}.nii.gz"
171
+
172
+ Copy one file out of each directory whose path names a subject:
173
+
174
+ pfdo_run \\
175
+ --inputDir $PWD/raw --outputDir $PWD/out \\
176
+ --dirFilter 100307 \\
177
+ --exec "cp %{inputWorkingDir}/brain.mgz %{outputWorkingDir}/brain.mgz" \\
178
+ --analyzeFileIndex f --noJobLogging --json
179
+
180
+ """
181
+
182
+ CLI_FULL: str = CLI_IO_CORE + CLI_SELF + PFDO_CLI_SELF + CLI_CORE
183
+ ARGS_SYNOPSIS_FULL: str = ARGS_SYNOPSIS_IO + ARGS_SYNOPSIS_SELF + PFDO_ARGS_SYNOPSIS_SELF + ARGS_SYNOPSIS_CORE
184
+ CLI_DS: str = CLI_DS_IO + CLI_SELF + PFDO_CLI_SELF + CLI_CORE
185
+ ARGS_SYNOPSIS_DS: str = ARGS_SYNOPSIS_DS + ARGS_SYNOPSIS_SELF + PFDO_ARGS_SYNOPSIS_SELF + ARGS_SYNOPSIS_CORE
186
+
187
+ DESCRIPTION: str = """
188
+ DESCRIPTION
189
+
190
+ ``pfdo_run`` walks the input tree with pftree, selects files in each
191
+ directory with the pfdo filters and --analyzeFileIndex, and runs the
192
+ --exec command once per selected file, with the command's tags
193
+ resolved against that directory and file. The output tree mirrors
194
+ the input tree; each directory's results and job logs land in its
195
+ counterpart. Directories run in parallel under --threads.
196
+ """
197
+
198
+
199
+ def synopsis(shortOnly: bool = False, command: str = "pfdo_run") -> str:
200
+ """Assemble the manual text.
201
+
202
+ Args:
203
+ shortOnly: NAME, SYNOPSIS and a brief example only.
204
+ command: the command name shown, for a package built on pfdo_run.
205
+
206
+ Returns:
207
+ The manual text, without the banner.
208
+ """
209
+ shortSynopsis: str = (
210
+ f"""
211
+ NAME
212
+
213
+ {command}
214
+
215
+ SYNOPSIS
216
+
217
+ {command} \\"""
218
+ + CLI_FULL
219
+ + f"""
220
+
221
+ BRIEF EXAMPLE
222
+
223
+ {command} \\
224
+ --inputDir /var/www/html/data --fileFilter jpg \\
225
+ --outputDir /var/www/html/png \\
226
+ --exec "convert %{{inputWorkingDir}}/%{{inputWorkingFile}}
227
+ %{{outputWorkingDir}}/%{{inputWorkingFile|rmext}}.png" \\
228
+ --threads 4 --printElapsedTime
229
+ """
230
+ )
231
+ if shortOnly:
232
+ return shortSynopsis
233
+ return shortSynopsis + DESCRIPTION + "\n ARGS " + ARGS_SYNOPSIS_FULL + TAG_HELP + EXAMPLES
234
+
235
+
236
+ def manual_build() -> Manual:
237
+ """The pfdo_run manual page."""
238
+ return Manual(
239
+ command="pfdo_run",
240
+ art=BANNER_ART,
241
+ title=BANNER_TITLE,
242
+ text=BANNER_TEXT,
243
+ version=__version__,
244
+ synopsisShort=synopsis(shortOnly=True),
245
+ synopsisFull=synopsis(shortOnly=False),
246
+ )
247
+
248
+
249
+ def parserSelf_build() -> argparse.ArgumentParser:
250
+ """pfdo_run's own options.
251
+
252
+ Returns:
253
+ A parent parser with ``--exec`` and ``--noJobLogging``.
254
+ """
255
+ parser: argparse.ArgumentParser = argparse.ArgumentParser(add_help=False)
256
+ parser.add_argument(
257
+ "--exec", dest="exec", default="", help="the command template to run per file (required)"
258
+ )
259
+ parser.add_argument(
260
+ "--noJobLogging",
261
+ dest="noJobLogging",
262
+ action="store_true",
263
+ help="do not write per-job cmd/stdout/stderr/rc",
264
+ )
265
+ return parser
266
+
267
+
268
+ def parser_build(standalone: bool = True) -> argparse.ArgumentParser:
269
+ """Assemble the full parser.
270
+
271
+ Args:
272
+ standalone: include ``--inputDir`` / ``--outputDir``. A ChRIS DS plugin
273
+ passes False and adds its own positional directories.
274
+
275
+ Returns:
276
+ The parser.
277
+ """
278
+ parents: list[argparse.ArgumentParser] = [
279
+ parserCore_build("pfdo_run", __version__),
280
+ pfdoParserSelf_build(),
281
+ parserSelf_build(),
282
+ ]
283
+ if standalone:
284
+ parents.insert(0, parserIO_build())
285
+ return argparse.ArgumentParser(
286
+ prog="pfdo_run",
287
+ description="Run a command in every directory of a tree. Full manual: pfdo_run --man",
288
+ formatter_class=argparse.RawDescriptionHelpFormatter,
289
+ parents=parents,
290
+ )
291
+
292
+
293
+ def options_fromArgs(args: argparse.Namespace) -> PfdoRunOptions:
294
+ """Turn parsed arguments into options."""
295
+ return PfdoRunOptions.fromDict(vars(args))
296
+
297
+
298
+ def result_report(args: argparse.Namespace, result: RunResult) -> None:
299
+ """Print what the arguments asked for."""
300
+ if args.json:
301
+ print(json.dumps(result.toDict(), indent=4, sort_keys=True))
302
+ if args.printElapsedTime:
303
+ print(f"Elapsed time = {result.runTime:.6f} seconds")
304
+
305
+
306
+ def main(argv: Sequence[str] | None = None) -> int:
307
+ """Run the command.
308
+
309
+ Args:
310
+ argv: arguments without the program name; ``None`` reads ``sys.argv``.
311
+
312
+ Returns:
313
+ 0 when at least one directory's jobs all succeeded (pftree's stage rule);
314
+ 1 when the run failed everywhere or could not start; 2 on bad arguments.
315
+ """
316
+ parser: argparse.ArgumentParser = parser_build()
317
+ args: argparse.Namespace = parser.parse_args(argv)
318
+ if args.man or args.synopsis:
319
+ manual_print(manual_build(), shortOnly=not args.man)
320
+ return 0
321
+ if not args.inputDir:
322
+ parser.error("--inputDir is required")
323
+ if not args.exec:
324
+ parser.error("--exec is required")
325
+ try:
326
+ options: PfdoRunOptions = options_fromArgs(args)
327
+ except ValueError as err:
328
+ parser.error(str(err))
329
+ logging_configure(options.verbosity)
330
+ result: RunResult = PfdoRun(options).run()
331
+ result_report(args, result)
332
+ if result.error:
333
+ print(result.error, file=sys.stderr)
334
+ return 0 if result.status else 1
335
+
336
+
337
+ if __name__ == "__main__":
338
+ sys.exit(main())
@@ -0,0 +1,92 @@
1
+ """The data models of pfdo_run.
2
+
3
+ pfdo_run adds two options to pfdo's: the command template to run in every directory,
4
+ and whether each job's record is written beside its results. A job is one run of the
5
+ command for one file; its record names the file, the command as built, and carries the
6
+ hostlib envelope of the run when the command ran.
7
+
8
+ pfdo's and hostlib's models are re-exported here so that a tool built on pfdo_run
9
+ imports from pfdo_run alone.
10
+
11
+ Key components:
12
+ - PfdoRunOptions: ``PfdoOptions`` plus ``exec`` and ``noJobLogging``
13
+ - FileJob: one file's job: the command built for it and, if it ran, the run's envelope
14
+ - DirNode, StageResult, ProcessResult, RunResult, PfdoOptions, TreeOptions: pfdo's models
15
+ - JobResult, JobSpec, HostFacts: hostlib's models
16
+
17
+ Typical usage:
18
+ options = PfdoRunOptions.fromDict(vars(args))
19
+ PfdoRun(options).run()
20
+
21
+ Dependencies:
22
+ - pfdo.models, hostlib.models
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass, field
28
+
29
+ from hostlib.models import HostFacts, JobResult, JobSpec
30
+ from pfdo.models import DirNode, PfdoOptions, ProcessResult, RunResult, StageResult, TreeOptions
31
+
32
+
33
+ @dataclass
34
+ class PfdoRunOptions(PfdoOptions):
35
+ """pfdo's options plus pfdo_run's own.
36
+
37
+ Attributes:
38
+ exec: the command template, a pftag template split like a shell command.
39
+ Its tags name the current directory's paths and file:
40
+ ``%{inputWorkingDir}``, ``%{outputWorkingDir}``, ``%{inputWorkingFile}``,
41
+ plus every option by name, ``%{outputDir}``.
42
+ noJobLogging: do not write each job's command, stdout, stderr and return code
43
+ beside its results.
44
+
45
+ Example:
46
+ >>> PfdoRunOptions.fromDict({"inputDir": "/in", "exec": "ls %{inputWorkingDir}"}).exec
47
+ 'ls %{inputWorkingDir}'
48
+ """
49
+
50
+ exec: str = ""
51
+ noJobLogging: bool = False
52
+
53
+
54
+ @dataclass
55
+ class FileJob:
56
+ """One file's job: the command built for it and, when it ran, the run.
57
+
58
+ Attributes:
59
+ file: the input file the job was built for.
60
+ argv: the command as built, after tag substitution; the template's own
61
+ arguments when it could not be resolved; empty when there was no template.
62
+ status: the command was built, started and exited 0.
63
+ errors: why not: a tag that did not resolve, a program that could not start,
64
+ a non-zero exit status.
65
+ run: hostlib's envelope of the run, with both streams, exit status, host facts
66
+ and timing; ``None`` when the command was never run.
67
+
68
+ Example:
69
+ >>> FileJob("a.txt", ["true"], True).run is None
70
+ True
71
+ """
72
+
73
+ file: str
74
+ argv: list[str]
75
+ status: bool
76
+ errors: list[str] = field(default_factory=list)
77
+ run: JobResult | None = None
78
+
79
+
80
+ __all__: list[str] = [
81
+ "DirNode",
82
+ "FileJob",
83
+ "HostFacts",
84
+ "JobResult",
85
+ "JobSpec",
86
+ "PfdoOptions",
87
+ "PfdoRunOptions",
88
+ "ProcessResult",
89
+ "RunResult",
90
+ "StageResult",
91
+ "TreeOptions",
92
+ ]