pfdo 6.0.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.
pfdo/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ """pfdo: walk a directory tree with pftree and do something in every directory.
2
+
3
+ The base class of a family of tools, each doing one thing per directory of an input
4
+ tree and writing results to the same place in an output tree. ``Pfdo`` is a
5
+ ``Pftree`` whose callbacks are meant to be overridden, plus the family's shared
6
+ ``--analyzeFileIndex`` choice of which files the analyze step gets.
7
+
8
+ Key components:
9
+ - Pfdo: the base class
10
+ - PfdoOptions: pftree's options plus pfdo's
11
+ - fileIndex_select: the ``--analyzeFileIndex`` rule
12
+ - __main__: the ``pfdo`` command line, its manual, and the parts a package
13
+ built on pfdo assembles its own from
14
+
15
+ Typical usage:
16
+ from pfdo import Pfdo, PfdoOptions
17
+ Pfdo(PfdoOptions(inputDir=Path("/in"), outputDir=Path("/out"), testRun=True)).run()
18
+
19
+ Dependencies:
20
+ - pftree
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ __version__: str = "6.0.0"
26
+
27
+ from pfdo.models import PfdoOptions, fileIndex_validate
28
+ from pfdo.pfdo import ANALYZED_PREFIX, Pfdo, fileIndex_select
29
+
30
+ __all__ = [
31
+ "ANALYZED_PREFIX",
32
+ "Pfdo",
33
+ "PfdoOptions",
34
+ "__version__",
35
+ "fileIndex_select",
36
+ "fileIndex_validate",
37
+ ]
pfdo/__main__.py ADDED
@@ -0,0 +1,331 @@
1
+ """The ``pfdo`` command line and its manual.
2
+
3
+ ``pfdo`` on its own walks a tree, applies the filters, and with ``--test`` runs the
4
+ base class's demonstration callbacks. Its parser and manual are assembled from
5
+ pftree's parts plus pfdo's own, and are themselves exposed in parts so a package
6
+ built on pfdo can assemble its own.
7
+
8
+ Key components:
9
+ - CLI_SELF, ARGS_SYNOPSIS_SELF, EXAMPLES: pfdo's manual pieces
10
+ - CLI_FULL, CLI_DS, ARGS_SYNOPSIS_FULL, ARGS_SYNOPSIS_DS: the assembled pieces,
11
+ standalone and ChRIS-DS style
12
+ - synopsis: the manual text for a command built on pfdo
13
+ - manual_build: the page, rendered through ``pftree.manual``
14
+ - parserSelf_build / parser_build: the argument parsers
15
+ - main: the entry point, honouring an explicit ``argv``
16
+
17
+ Typical usage:
18
+ pfdo --inputDir /data --outputDir /tmp/out --fileFilter jpg --test --json
19
+ pfdo --man
20
+
21
+ Dependencies:
22
+ - pftree (its CLI parts and manual renderer)
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import json
29
+ import sys
30
+ from collections.abc import Sequence
31
+
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
+ from pftree.models import RunResult
45
+
46
+ from pfdo import __version__
47
+ from pfdo.models import PfdoOptions
48
+ from pfdo.pfdo import Pfdo
49
+
50
+ BANNER_ART: str = r"""
51
+
52
+ __ _
53
+ / _| | |
54
+ _ __ | |_ __| | ___
55
+ | '_ \ | _| / _` | / _ \
56
+ | |_) || | | (_| || (_) |
57
+ | .__/ |_| \__,_| \___/
58
+ | |
59
+ |_|
60
+
61
+ """
62
+
63
+ BANNER_TITLE: str = "Path-File Do (something)"
64
+
65
+ BANNER_TEXT: str = """
66
+ Path-File Do (something)
67
+
68
+ Recursively walk down a directory tree and perform some operation
69
+ on files in each directory (optionally filtered by some simple
70
+ expression). Results of each operation are saved in output tree
71
+ that preserves the input directory structure.
72
+
73
+ -- version {version} --
74
+
75
+ 'pfdo' is the base infrastructure app/class for walking down some
76
+ dir tree, optionally finding and tagging files that conform to
77
+ a simple filter, and running (something) on the files. This class
78
+ provides the basic mechanism for using pftree in this manner and the
79
+ (something) operation should be provided by a subclass of this base.
80
+
81
+ As part of the "pf*" suite of applications, it is geared to IO as
82
+ directories. Nested directory trees within some input directory
83
+ are reconstructed in an output directory, preserving directory
84
+ structure.
85
+
86
+ In many ways, this module is a slight variation on 'pftree' on which
87
+ it builds. It can be thought of as less of a descendent and more of a
88
+ sibling.
89
+
90
+ """
91
+
92
+ CLI_SELF: str = """
93
+ [--analyzeFileIndex <a|f|l|m|N>] \\
94
+ [--test] \\"""
95
+
96
+ ARGS_SYNOPSIS_SELF: str = """
97
+ [--test]
98
+ If specified, run the "dummy" internal callback loop triad. The test
99
+ flow simply tags files in some inputDir tree and "touches" them to a
100
+ reconstituted tree in the output directory, prefixed with the text
101
+ "analyzed-". An output file that already exists stops that directory
102
+ unless --overwrite is given.
103
+
104
+ [--analyzeFileIndex <a|f|l|m|N>]
105
+ An optional string to further "filter" the list of files that have
106
+ been detected to analyze. This is useful for certain operations
107
+ that might need only one file in an input directory to start but
108
+ then once started will self consume multiple files independently
109
+ of the external controller. The default is "a" which implies
110
+ *ALL* files in a given directory. The set of valid values:
111
+
112
+ "a": "all" files.
113
+ "m": only the "middle" file in the file list
114
+ "f": only the "first" file in the file list
115
+ "l": only the "last" file in the file list
116
+ "<N>": only the file at index N in the file list. This index
117
+ can "wrap" in the positive and negative direction and
118
+ can exceed the actual list length (in which case it is
119
+ mapped back into the original list).
120
+
121
+ Pedantically, in the negative direction:
122
+ '-1' will mean the last file, while
123
+ -<listLen> will mean the first file again
124
+ -<listLen+1> will mean the last file again
125
+ etc...
126
+ and in the positive direction:
127
+ <listLen> will mean the first file
128
+ <listLen>*2 will mean the first file
129
+ etc...
130
+ """
131
+
132
+ EXAMPLES: str = """
133
+
134
+ EXAMPLES
135
+
136
+ Perform a `pfdo` down some input directory:
137
+
138
+ pfdo \\
139
+ --inputDir /var/www/html/data \\
140
+ --fileFilter jpg \\
141
+ --outputDir /tmp/jpg \\
142
+ --test --json \\
143
+ --threads 0 --printElapsedTime
144
+
145
+ The above will find all files in the tree structure rooted at
146
+ /var/www/html/data that also contain the string "jpg" anywhere
147
+ in the filename. For each file found, a corresponding file will
148
+ be touched in the output directory, in the same tree location as
149
+ the original input. This touched file will be prefixed with the
150
+ string "analyzed-".
151
+
152
+ pfdo \\
153
+ --inputDir $PWD/raw \\
154
+ --dirFilter 100307 \\
155
+ --outputDir $PWD/out --test --json \\
156
+ --analyzeFileIndex m \\
157
+ --threads 0 --printElapsedTime
158
+
159
+ Here, only the middle file in (all) directories whose path contains
160
+ the string ``100307`` will be targetted.
161
+
162
+ Finally the elapsed time and a JSON output are printed.
163
+
164
+ """
165
+
166
+ CLI_FULL: str = CLI_IO_CORE + CLI_SELF + CLI_CORE
167
+ ARGS_SYNOPSIS_FULL: str = ARGS_SYNOPSIS_IO + ARGS_SYNOPSIS_SELF + ARGS_SYNOPSIS_CORE
168
+ CLI_DS: str = CLI_DS_IO + CLI_SELF + CLI_CORE
169
+ ARGS_SYNOPSIS_DS: str = ARGS_SYNOPSIS_DS + ARGS_SYNOPSIS_SELF + ARGS_SYNOPSIS_CORE
170
+
171
+ DESCRIPTION: str = """
172
+ DESCRIPTION
173
+
174
+ ``pfdo`` provides the base mechanism for navigating some arbitrary
175
+ tree, providing the base hooks for operating on (possibly filtered)
176
+ files in each directory, and saving results in an output tree that
177
+ reflects the input tree topology.
178
+
179
+ A subclass overrides the inputAnalyze_callback and outputSave_callback
180
+ methods (each receives the DirNode for a directory and returns a
181
+ StageResult) and sets processAlways; ``pfdo`` itself only runs its
182
+ demonstration callbacks under --test.
183
+ """
184
+
185
+
186
+ def synopsis(shortOnly: bool = False, command: str = "pfdo") -> str:
187
+ """Assemble the manual text.
188
+
189
+ Args:
190
+ shortOnly: NAME, SYNOPSIS and a brief example only.
191
+ command: the command name shown, for a package built on pfdo.
192
+
193
+ Returns:
194
+ The manual text, without the banner.
195
+ """
196
+ shortSynopsis: str = (
197
+ f"""
198
+ NAME
199
+
200
+ {command}
201
+
202
+ SYNOPSIS
203
+
204
+ {command} \\"""
205
+ + CLI_FULL
206
+ + f"""
207
+
208
+ BRIEF EXAMPLE
209
+
210
+ {command} \\
211
+ --inputDir /var/www/html/data \\
212
+ --fileFilter jpg \\
213
+ --outputDir /var/www/html/jpg \\
214
+ --threads 0 --printElapsedTime --test
215
+ """
216
+ )
217
+ if shortOnly:
218
+ return shortSynopsis
219
+ return shortSynopsis + DESCRIPTION + "\n ARGS " + ARGS_SYNOPSIS_FULL + EXAMPLES
220
+
221
+
222
+ def manual_build() -> Manual:
223
+ """The pfdo manual page.
224
+
225
+ Returns:
226
+ The page, with both synopses assembled.
227
+ """
228
+ return Manual(
229
+ command="pfdo",
230
+ art=BANNER_ART,
231
+ title=BANNER_TITLE,
232
+ text=BANNER_TEXT,
233
+ version=__version__,
234
+ synopsisShort=synopsis(shortOnly=True),
235
+ synopsisFull=synopsis(shortOnly=False),
236
+ )
237
+
238
+
239
+ def parserSelf_build() -> argparse.ArgumentParser:
240
+ """pfdo's own options.
241
+
242
+ Returns:
243
+ A parent parser with ``--test`` and ``--analyzeFileIndex``.
244
+ """
245
+ parser: argparse.ArgumentParser = argparse.ArgumentParser(add_help=False)
246
+ parser.add_argument("--test", dest="testRun", action="store_true", help="run the demonstration callbacks")
247
+ parser.add_argument(
248
+ "--analyzeFileIndex",
249
+ dest="analyzeFileIndex",
250
+ default="a",
251
+ help="which files per directory the analyze step gets: a (all), f, l, m, or an index",
252
+ )
253
+ return parser
254
+
255
+
256
+ def parser_build(standalone: bool = True) -> argparse.ArgumentParser:
257
+ """Assemble the full parser.
258
+
259
+ Args:
260
+ standalone: include ``--inputDir`` / ``--outputDir``. A ChRIS DS plugin
261
+ passes False and adds its own positional directories.
262
+
263
+ Returns:
264
+ The parser; ``--help`` is the quick reference and points at ``--man``.
265
+ """
266
+ parents: list[argparse.ArgumentParser] = [parserCore_build("pfdo", __version__), parserSelf_build()]
267
+ if standalone:
268
+ parents.insert(0, parserIO_build())
269
+ return argparse.ArgumentParser(
270
+ prog="pfdo",
271
+ description="Walk a directory tree and do something in every directory. Full manual: pfdo --man",
272
+ formatter_class=argparse.RawDescriptionHelpFormatter,
273
+ parents=parents,
274
+ )
275
+
276
+
277
+ def options_fromArgs(args: argparse.Namespace) -> PfdoOptions:
278
+ """Turn parsed arguments into options.
279
+
280
+ Args:
281
+ args: the parser's Namespace.
282
+
283
+ Returns:
284
+ PfdoOptions with every known field applied.
285
+ """
286
+ return PfdoOptions.fromDict(vars(args))
287
+
288
+
289
+ def result_report(args: argparse.Namespace, result: RunResult) -> None:
290
+ """Print what the arguments asked for.
291
+
292
+ Args:
293
+ args: the parsed arguments.
294
+ result: the run's result.
295
+ """
296
+ if args.json:
297
+ print(json.dumps(result.toDict(), indent=4, sort_keys=True))
298
+ if args.printElapsedTime:
299
+ print(f"Elapsed time = {result.runTime:.6f} seconds")
300
+
301
+
302
+ def main(argv: Sequence[str] | None = None) -> int:
303
+ """Run the command.
304
+
305
+ Args:
306
+ argv: arguments without the program name; ``None`` reads ``sys.argv``.
307
+
308
+ Returns:
309
+ The exit code: 0 on success, 1 when the run failed, 2 on bad arguments.
310
+ """
311
+ parser: argparse.ArgumentParser = parser_build()
312
+ args: argparse.Namespace = parser.parse_args(argv)
313
+ if args.man or args.synopsis:
314
+ manual_print(manual_build(), shortOnly=not args.man)
315
+ return 0
316
+ if not args.inputDir:
317
+ parser.error("--inputDir is required")
318
+ try:
319
+ options: PfdoOptions = options_fromArgs(args)
320
+ except ValueError as err:
321
+ parser.error(str(err))
322
+ logging_configure(options.verbosity)
323
+ result: RunResult = Pfdo(options).run()
324
+ result_report(args, result)
325
+ if result.error:
326
+ print(result.error, file=sys.stderr)
327
+ return 0 if result.status else 1
328
+
329
+
330
+ if __name__ == "__main__":
331
+ sys.exit(main())
pfdo/models.py ADDED
@@ -0,0 +1,75 @@
1
+ """The data models of pfdo.
2
+
3
+ pfdo adds two options to pftree's: which file(s) in each directory the analyze
4
+ callback should work on, and whether the base class's demonstration callbacks run.
5
+ Everything else, from the input and output directories to the filters and threads,
6
+ is pftree's ``TreeOptions``.
7
+
8
+ Key components:
9
+ - PfdoOptions: ``TreeOptions`` plus ``analyzeFileIndex`` and ``testRun``
10
+ - fileIndex_validate: the grammar of ``--analyzeFileIndex``
11
+
12
+ Typical usage:
13
+ options = PfdoOptions.fromDict(vars(args))
14
+ Pfdo(options).run()
15
+
16
+ Dependencies:
17
+ - pftree.models
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from dataclasses import dataclass
23
+
24
+ from pftree.models import TreeOptions
25
+
26
+ FILE_INDEX_ALL: str = "a"
27
+ FILE_INDEX_FIRST: str = "f"
28
+ FILE_INDEX_LAST: str = "l"
29
+ FILE_INDEX_MIDDLE: str = "m"
30
+ FILE_INDEX_WORDS: tuple[str, ...] = (FILE_INDEX_ALL, FILE_INDEX_FIRST, FILE_INDEX_LAST, FILE_INDEX_MIDDLE)
31
+
32
+
33
+ def fileIndex_validate(spec: str) -> None:
34
+ """Check an ``--analyzeFileIndex`` value.
35
+
36
+ Args:
37
+ spec: ``a``, ``f``, ``l``, ``m`` or an integer, possibly negative.
38
+
39
+ Raises:
40
+ ValueError: for anything else.
41
+ """
42
+ if spec in FILE_INDEX_WORDS:
43
+ return
44
+ try:
45
+ int(spec)
46
+ except ValueError as err:
47
+ raise ValueError(f"analyzeFileIndex must be a, f, l, m or an integer, not {spec!r}") from err
48
+
49
+
50
+ @dataclass
51
+ class PfdoOptions(TreeOptions):
52
+ """pftree's options plus pfdo's own.
53
+
54
+ Attributes:
55
+ analyzeFileIndex: which of a directory's files the analyze callback gets:
56
+ ``a`` all (the default), ``f`` the first, ``l`` the last, ``m`` the
57
+ middle, or an integer index that wraps in either direction.
58
+ testRun: run the base class's demonstration callbacks over the tree.
59
+
60
+ Example:
61
+ >>> PfdoOptions.fromDict({"inputDir": "/in", "analyzeFileIndex": "-1"}).analyzeFileIndex
62
+ '-1'
63
+ """
64
+
65
+ analyzeFileIndex: str = FILE_INDEX_ALL
66
+ testRun: bool = False
67
+
68
+ def __post_init__(self) -> None:
69
+ """Validate pftree's options and then ``analyzeFileIndex``.
70
+
71
+ Raises:
72
+ ValueError: on a bad filter, leaf template or file index.
73
+ """
74
+ super().__post_init__()
75
+ fileIndex_validate(self.analyzeFileIndex)
pfdo/pfdo.py ADDED
@@ -0,0 +1,200 @@
1
+ """pfdo: walk a tree with pftree and do something in every directory.
2
+
3
+ ``Pfdo`` is the base class of a family of tools (pfdo_run, pfdo_med2image and
4
+ others) that each do one thing per directory of an input tree and write their
5
+ results to the same place in an output tree. It is a ``Pftree`` whose three
6
+ callbacks are meant to be overridden, plus one convenience the family shares: the
7
+ ``--analyzeFileIndex`` choice of which of a directory's files the analyze step gets.
8
+
9
+ The base class's own callbacks are a demonstration: read lists the files, analyze
10
+ picks by index and prefixes each name with ``analyzed-``, save touches those names
11
+ in the output directory. ``pfdo --test`` runs them.
12
+
13
+ Key components:
14
+ - Pfdo: the base class; override ``inputAnalyze_callback`` and
15
+ ``outputSave_callback``, set ``processAlways`` to True
16
+ - fileIndex_select: the ``--analyzeFileIndex`` rule, as a pure function
17
+
18
+ Typical usage:
19
+ class Mine(Pfdo):
20
+ processAlways = True
21
+
22
+ def inputAnalyze_callback(self, node: DirNode, read: StageResult) -> StageResult:
23
+ for name in self.filesToAnalyze(node):
24
+ ...
25
+
26
+ Mine(PfdoOptions.fromDict(vars(args))).run()
27
+
28
+ Dependencies:
29
+ - pftree
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import logging
35
+ from pathlib import Path
36
+
37
+ from pftree.models import DirNode, ProcessResult, RunResult, StageResult
38
+ from pftree.tree import Pftree
39
+
40
+ from pfdo.models import (
41
+ FILE_INDEX_ALL,
42
+ FILE_INDEX_FIRST,
43
+ FILE_INDEX_LAST,
44
+ FILE_INDEX_MIDDLE,
45
+ PfdoOptions,
46
+ )
47
+
48
+ logger: logging.Logger = logging.getLogger("pfdo")
49
+
50
+ ANALYZED_PREFIX: str = "analyzed-"
51
+
52
+
53
+ def fileIndex_select(files: list[str], spec: str) -> list[str]:
54
+ """Pick the files ``--analyzeFileIndex`` names out of a directory's list.
55
+
56
+ Args:
57
+ files: the directory's file names, in order.
58
+ spec: ``a`` for all, ``f`` first, ``l`` last, ``m`` middle, or an integer.
59
+ An integer wraps: ``-1`` is the last file, ``len(files)`` the first again.
60
+
61
+ Returns:
62
+ The chosen files, in their original order; empty when ``files`` is empty.
63
+
64
+ Raises:
65
+ ValueError: if ``spec`` is none of the above.
66
+
67
+ Example:
68
+ >>> fileIndex_select(["a", "b", "c"], "m")
69
+ ['b']
70
+ >>> fileIndex_select(["a", "b", "c"], "-1")
71
+ ['c']
72
+ """
73
+ if not files:
74
+ return []
75
+ match spec:
76
+ case s if s == FILE_INDEX_ALL:
77
+ return list(files)
78
+ case s if s == FILE_INDEX_FIRST:
79
+ return [files[0]]
80
+ case s if s == FILE_INDEX_LAST:
81
+ return [files[-1]]
82
+ case s if s == FILE_INDEX_MIDDLE:
83
+ return [files[len(files) // 2]]
84
+ case _:
85
+ try:
86
+ index: int = int(spec)
87
+ except ValueError as err:
88
+ raise ValueError(f"analyzeFileIndex must be a, f, l, m or an integer, not {spec!r}") from err
89
+ return [files[index % len(files)]]
90
+
91
+
92
+ class Pfdo(Pftree):
93
+ """Walk an input tree and run a per-directory read / analyze / save sequence.
94
+
95
+ Attributes:
96
+ options: the PfdoOptions in force.
97
+ processAlways: a subclass sets this True so ``run`` always runs the
98
+ callbacks; the base class runs them only under ``--test``.
99
+
100
+ Example:
101
+ >>> tool = Pfdo(PfdoOptions(inputDir=Path("."), outputDir=Path("/tmp/out"), verbosity=0))
102
+ >>> tool.run().status
103
+ True
104
+ """
105
+
106
+ processAlways: bool = False
107
+
108
+ def __init__(self, options: PfdoOptions) -> None:
109
+ super().__init__(options)
110
+ self.options: PfdoOptions = options
111
+
112
+ def filesToAnalyze(self, node: DirNode) -> list[str]:
113
+ """The files of a directory that ``--analyzeFileIndex`` selects.
114
+
115
+ Args:
116
+ node: the directory.
117
+
118
+ Returns:
119
+ The selected names, in order.
120
+ """
121
+ return fileIndex_select(node.files, self.options.analyzeFileIndex)
122
+
123
+ def inputRead_callback(self, node: DirNode) -> StageResult:
124
+ """Demonstration read: list the directory's files without opening them.
125
+
126
+ Args:
127
+ node: the directory.
128
+
129
+ Returns:
130
+ Success with the files; failure if there are none.
131
+ """
132
+ logger.debug("reading %s: %d files", node.relPath, len(node.files))
133
+ return StageResult(status=bool(node.files), files=list(node.files))
134
+
135
+ def inputAnalyze_callback(self, node: DirNode, read: StageResult) -> StageResult:
136
+ """Demonstration analyze: select by index and tag each name.
137
+
138
+ Args:
139
+ node: the directory.
140
+ read: the read stage's result.
141
+
142
+ Returns:
143
+ Success with ``analyzed-<name>`` for every selected file; failure when
144
+ the index selects nothing.
145
+ """
146
+ selected: list[str] = fileIndex_select(read.files, self.options.analyzeFileIndex)
147
+ logger.debug("analyzing %s: %s", node.relPath, selected)
148
+ return StageResult(status=bool(selected), files=[ANALYZED_PREFIX + name for name in selected])
149
+
150
+ def outputSave_callback(self, node: DirNode, analyze: StageResult) -> StageResult:
151
+ """Demonstration save: touch each analyzed name in the output directory.
152
+
153
+ Args:
154
+ node: the directory; its ``outputPath`` is created.
155
+ analyze: the analyze stage's result.
156
+
157
+ Returns:
158
+ Success with the names written. An existing file stops the directory
159
+ with a failure unless ``overwrite`` is set.
160
+ """
161
+ node.outputPath.mkdir(parents=True, exist_ok=True)
162
+ saved: list[str] = []
163
+ for name in analyze.files:
164
+ target: Path = node.outputPath / name
165
+ if target.exists() and not self.options.overwrite:
166
+ logger.error("%s exists; run with --overwrite to replace it", target)
167
+ return StageResult(status=False, files=saved)
168
+ target.touch()
169
+ saved.append(name)
170
+ logger.debug("saved %d files in %s", len(saved), node.outputPath)
171
+ return StageResult(status=True, files=saved)
172
+
173
+ def callbacks_run(self) -> ProcessResult:
174
+ """Run the three callbacks over the tree."""
175
+ return self.tree_process(
176
+ inputRead_callback=self.inputRead_callback,
177
+ inputAnalyze_callback=self.inputAnalyze_callback,
178
+ outputWrite_callback=self.outputSave_callback,
179
+ )
180
+
181
+ def run(self) -> RunResult:
182
+ """Probe, filter, then process when the class or ``--test`` asks for it.
183
+
184
+ Returns:
185
+ The run's result; ``status`` is False with ``error`` set when the input
186
+ directory is missing, or when processing failed in every directory.
187
+ """
188
+ error: str = self.env_check()
189
+ if error:
190
+ logger.error(error)
191
+ return RunResult(status=False, error=error, runTime=self.runTime_get())
192
+ self.tree_construct(self.tree_probe())
193
+ if self.fileTokens or self.dirTokens:
194
+ self.tree_filter()
195
+ process: ProcessResult | None = None
196
+ status: bool = True
197
+ if self.processAlways or self.options.testRun:
198
+ process = self.callbacks_run()
199
+ status = process.status
200
+ return RunResult(status=status, tree=dict(self.tree), process=process, runTime=self.runTime_get())
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: pfdo
3
+ Version: 6.0.0
4
+ Summary: Walk a directory tree with pftree and do something in every directory: the base class of the pfdo family
5
+ Author-email: FNNDSC <dev@babyMRI.org>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/FNNDSC/pfdo
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: pftree>=4.1.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=8; extra == "dev"
21
+ Requires-Dist: ruff>=0.6; extra == "dev"
22
+ Dynamic: license-file
23
+
24
+ pfdo
25
+ ====
26
+
27
+ .. image:: https://github.com/FNNDSC/pfdo/actions/workflows/ci.yml/badge.svg
28
+ :target: https://github.com/FNNDSC/pfdo/actions/workflows/ci.yml
29
+
30
+ .. image:: https://img.shields.io/pypi/v/pfdo.svg
31
+ :target: https://pypi.org/project/pfdo/
32
+
33
+ .. image:: https://img.shields.io/badge/python-3.11%2B-blue.svg
34
+ :target: https://pypi.org/project/pfdo/
35
+
36
+ Walk a directory tree with ``pftree`` and do something in every directory: the base class of the pfdo family.
37
+
38
+ .. contents:: Table of Contents
39
+
40
+ Overview
41
+ --------
42
+
43
+ ``pfdo`` is the base class of a family of tools (``pfdo_run``, ``pfdo_med2image``, ``pfdo_mgz2image`` and others) that each do one thing per directory of an input tree and write their results to the same place in an output tree. It is a ``pftree.Pftree`` whose three callbacks, read, analyze and save, are meant to be overridden, plus one convenience the family shares: ``--analyzeFileIndex``, the choice of which of a directory's files the analyze step gets.
44
+
45
+ On its own, ``pfdo`` walks a tree, applies ``pftree``'s filters, and with ``--test`` runs its demonstration callbacks: read lists each directory's files, analyze selects by index and prefixes every name with ``analyzed-``, save touches those names in the output directory.
46
+
47
+ Installation
48
+ ------------
49
+
50
+ .. code:: bash
51
+
52
+ pip install pfdo
53
+
54
+ Python 3.11 or newer; depends on ``pftree``.
55
+
56
+ Command line
57
+ ------------
58
+
59
+ .. code:: bash
60
+
61
+ pfdo --inputDir /data --outputDir /tmp/out --fileFilter jpg --test --json
62
+ pfdo --inputDir /data --outputDir /tmp/out --dirFilter 100307 --analyzeFileIndex m --test
63
+ pfdo --man
64
+
65
+ ``pfdo --man`` prints the full manual, ``--synopsis`` its first page, ``--help`` the quick reference. All of ``pftree``'s options apply (``--fileFilter``, ``--dirFilter`` and their logic, ``--maxDepth``, ``--outputLeafDir``, ``--threads``, ``--relativeDir``, ``--followLinks``, ``--overwrite``, ``--json``, ``--printElapsedTime``); ``pfdo`` adds:
66
+
67
+ ``--analyzeFileIndex <a|f|l|m|N>``
68
+ Which of a directory's files the analyze step receives: ``a`` all (default), ``f`` first, ``l`` last, ``m`` middle, or an integer index that wraps in either direction (``-1`` is the last file, ``len`` the first again).
69
+
70
+ ``--test``
71
+ Run the demonstration callbacks. An output file that already exists stops that directory unless ``--overwrite`` is given.
72
+
73
+ Library
74
+ -------
75
+
76
+ .. code:: python
77
+
78
+ from pathlib import Path
79
+ from pftree.models import DirNode, StageResult
80
+ from pfdo import Pfdo, PfdoOptions
81
+
82
+ class Counter(Pfdo):
83
+ processAlways = True
84
+
85
+ def inputAnalyze_callback(self, node: DirNode, read: StageResult) -> StageResult:
86
+ chosen = self.filesToAnalyze(node)
87
+ return StageResult(status=True, files=chosen, payload={"count": len(chosen)})
88
+
89
+ def outputSave_callback(self, node: DirNode, analyze: StageResult) -> StageResult:
90
+ node.outputPath.mkdir(parents=True, exist_ok=True)
91
+ (node.outputPath / "count.txt").write_text(f"{analyze.payload['count']}\n")
92
+ return StageResult(status=True, files=["count.txt"])
93
+
94
+ result = Counter(PfdoOptions(inputDir=Path("/data"), outputDir=Path("/out"), threads=4)).run()
95
+
96
+ ``PfdoOptions`` is ``pftree.TreeOptions`` plus ``analyzeFileIndex`` and ``testRun``; ``PfdoOptions.fromDict(vars(args))`` builds it from an argparse ``Namespace`` and ignores keys it does not know. A subclass sets ``processAlways = True`` so ``run()`` always runs the callbacks; it calls ``self.filesToAnalyze(node)`` to honour ``--analyzeFileIndex``. ``run()`` returns ``pftree``'s ``RunResult`` (``status``, ``tree``, ``process``, ``runTime``).
97
+
98
+ For its command line, a package built on pfdo composes ``pfdo.__main__``'s parts: ``parserSelf_build()`` and ``pftree``'s ``parserCore_build()`` / ``parserIO_build()`` for the parser, ``synopsis(command=...)`` or the ``CLI_*`` and ``ARGS_SYNOPSIS_*`` texts for the manual, and ``pftree.manual.Manual`` with ``manual_print`` to render it.
99
+
100
+ Changes in 6.0
101
+ --------------
102
+
103
+ - Built on ``pftree`` 4; ``pfmisc`` and ``pudb`` are no longer dependencies.
104
+ - The callbacks are ``inputRead_callback``, ``inputAnalyze_callback`` and ``outputSave_callback``, taking a ``DirNode`` and returning a ``StageResult``, not tuples and dicts.
105
+ - ``--analyzeFileIndex m`` picks the true middle file (index ``len // 2``); 5.x picked one past it for odd-length lists.
106
+ - An existing output file fails that directory and the run continues; 5.x stopped the loop and left the directory half written.
107
+ - ``--test`` maps to ``testRun``; ``pftree``'s own ``--test <seconds>`` is not exposed.
108
+ - ``main(argv)`` honours ``argv``; ``--version``, ``--man`` and ``--synopsis`` exit 0.
109
+ - Packaging is ``pyproject.toml`` only, published from CI on a ``v*`` tag.
@@ -0,0 +1,10 @@
1
+ pfdo/__init__.py,sha256=0EsMWOqP_UED0Py4sGGz2oRZG3UAGffB52QsUkhsq-M,1155
2
+ pfdo/__main__.py,sha256=Wtt9AfTr3NDMKU4uY6BXf2YaJrWKHXcL9OY461WyguU,11891
3
+ pfdo/models.py,sha256=dFUrH0pig7PH-VtYzRsrf5tlKUnXOjYE54l4NzkO-Hs,2262
4
+ pfdo/pfdo.py,sha256=uQ3pflXjjigs7KLG-MWl5hTNEnCq7GNqZHVQ_nIW1dU,7268
5
+ pfdo-6.0.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
6
+ pfdo-6.0.0.dist-info/METADATA,sha256=Mg_b52rRbCqJr7_iRgvVs9N0qCwDK3cDcGjVHekkyEQ,5617
7
+ pfdo-6.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
8
+ pfdo-6.0.0.dist-info/entry_points.txt,sha256=hybk6kcGUEbZEyns6EGrrG3gjNWOUikazp2OTdwiPEQ,44
9
+ pfdo-6.0.0.dist-info/top_level.txt,sha256=shX2HON2eRe6QKssvfYouSWv7aH5e61HqAWqN8D2vgk,5
10
+ pfdo-6.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pfdo = pfdo.__main__:main
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1 @@
1
+ pfdo