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.
- pfdo_run-4.0.0/PKG-INFO +110 -0
- pfdo_run-4.0.0/README.rst +85 -0
- pfdo_run-4.0.0/pfdo_run/__init__.py +39 -0
- pfdo_run-4.0.0/pfdo_run/__main__.py +338 -0
- pfdo_run-4.0.0/pfdo_run/models.py +92 -0
- pfdo_run-4.0.0/pfdo_run/pfdo_run.py +168 -0
- pfdo_run-4.0.0/pfdo_run.egg-info/PKG-INFO +110 -0
- {pfdo_run-3.2.6 → pfdo_run-4.0.0}/pfdo_run.egg-info/SOURCES.txt +5 -3
- pfdo_run-4.0.0/pfdo_run.egg-info/requires.txt +7 -0
- pfdo_run-4.0.0/pyproject.toml +47 -0
- pfdo_run-4.0.0/tests/test_cli.py +94 -0
- pfdo_run-4.0.0/tests/test_pfdo_run.py +227 -0
- pfdo_run-3.2.6/PKG-INFO +0 -267
- pfdo_run-3.2.6/README.rst +0 -254
- pfdo_run-3.2.6/pfdo_run/__init__.py +0 -4
- pfdo_run-3.2.6/pfdo_run/__main__.py +0 -304
- pfdo_run-3.2.6/pfdo_run/pfdo_run.py +0 -515
- pfdo_run-3.2.6/pfdo_run.egg-info/PKG-INFO +0 -267
- pfdo_run-3.2.6/pfdo_run.egg-info/not-zip-safe +0 -1
- pfdo_run-3.2.6/pfdo_run.egg-info/requires.txt +0 -4
- pfdo_run-3.2.6/setup.py +0 -48
- {pfdo_run-3.2.6 → pfdo_run-4.0.0}/LICENSE +0 -0
- {pfdo_run-3.2.6 → pfdo_run-4.0.0}/pfdo_run.egg-info/dependency_links.txt +0 -0
- {pfdo_run-3.2.6 → pfdo_run-4.0.0}/pfdo_run.egg-info/entry_points.txt +0 -0
- {pfdo_run-3.2.6 → pfdo_run-4.0.0}/pfdo_run.egg-info/top_level.txt +0 -0
- {pfdo_run-3.2.6 → pfdo_run-4.0.0}/setup.cfg +0 -0
pfdo_run-4.0.0/PKG-INFO
ADDED
|
@@ -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
|
+
]
|