penrun 0.2.1__tar.gz → 0.2.2__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.
penrun-0.2.2/PKG-INFO ADDED
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.3
2
+ Name: penrun
3
+ Version: 0.2.2
4
+ Summary: Run any command inside a well-defined, timestamped artifacts directory.
5
+ Author: AISEC Pentesting Team
6
+ License: Apache-2.0
7
+ Maintainer: Stefan Tatschner, Tobias Specht
8
+ Maintainer-email: Stefan Tatschner <stefan.tatschner@aisec.fraunhofer.de>, Tobias Specht <tobias.specht@aisec.fraunhofer.de>
9
+ Requires-Python: >=3.14
10
+ Description-Content-Type: text/markdown
11
+
12
+ <!--
13
+ SPDX-FileCopyrightText: AISEC Pentesting Team
14
+
15
+ SPDX-License-Identifier: CC0-1.0
16
+ -->
17
+
18
+ # penrun
19
+
20
+ This tool was initially rescued from the gallia [commit history](https://github.com/Fraunhofer-AISEC/gallia/commit/985664905b6ef352db6fdc895af2ac79492539da) and ported to Python.
21
+
22
+ `penrun` runs any command inside a well-defined, timestamped artifacts directory. Point it at a test,
23
+ scan, or anything else you invoke repeatedly, and every run gets its own directory with the command's
24
+ compressed output and exit code - so results are never overwritten and old runs stay reproducible.
25
+
26
+ ## Requirements
27
+
28
+ Python 3.14+, standard library only. No runtime dependencies.
29
+
30
+ ## Installation
31
+
32
+ ```console
33
+ $ uvx penrun -h
34
+ ```
35
+
36
+ Without installing, `uv run penrun` also works straight out of a checkout.
37
+
38
+ ## Quick start: normal mode
39
+
40
+ Run any command; `penrun` creates `penrun-artifacts/<command>/run-<timestamp>/` for it:
41
+
42
+ ```console
43
+ $ penrun ls -lah
44
+ $ tree penrun-artifacts/ls
45
+ penrun-artifacts/ls
46
+ ├── LATEST -> run-20260723-101334.123456
47
+ └── run-20260723-101334.123456
48
+ ├── META.json # command, start/end time, exit code, environment (with -e), error (on failure)
49
+ └── OUTPUT.zst # combined stdout+stderr, zstd-compressed
50
+ ```
51
+
52
+ `LATEST` always points at the most recent run of that command, regardless of tags or where
53
+ `artifacts_template` places the timestamped directory (see below).
54
+
55
+ `penrun` sets these environment variables for COMMAND:
56
+
57
+ - `PENRUN_COMMAND` - the command as run, shell-quoted.
58
+ - `PENRUN_ARTIFACTS` - the absolute path of this run's artifacts directory.
59
+ - `PENRUN_BATCH_MODE=1` - only in batch mode.
60
+ - `PENRUN_PWD_CONF`/`PENRUN_GIT_ROOT_CONF`/`PENRUN_USER_CONF` - path of each config file found while
61
+ searching (see [Configuration](#configuration)), regardless of which one was actually used.
62
+
63
+ Run `penrun -h` for the full list of flags.
64
+
65
+ ## Quick start: batch mode
66
+
67
+ Batch mode (`-b`) reads a command - or command fragment - per line from stdin and runs each one,
68
+ sequentially by default (`-j N` for up to `N` in parallel):
69
+
70
+ ```console
71
+ $ printf 'true\nfalse\ntrue\n' | penrun -b
72
+ ```
73
+
74
+ Pass `-T` with a template to turn plain input lines into full commands; `{1}` is replaced with the
75
+ line. Since every job below runs `curl`, add `-N` to name each job's directory after its input line
76
+ instead of the program name - otherwise they'd all land in the same `curl/` directory:
77
+
78
+ ```console
79
+ $ printf 'example.com\nexample.org\n' | penrun -b -N -T 'curl -Lo {1}.html https://{1}'
80
+ $ tree -L 2 penrun-artifacts/curl
81
+ penrun-artifacts/curl
82
+ ├── LATEST -> run-20260723-101334.123456
83
+ └── run-20260723-101334.123456
84
+ ├── example.com
85
+ └── example.org
86
+ ```
87
+
88
+ For programmatic callers that already know each job's command and artifacts directory name, `-J`/`--json`
89
+ reads newline-delimited JSON objects instead of plain lines - one `{"executable": "...", "args": [...],
90
+ "dirname": "..."}` per line (`dirname` is optional, falling back to `executable`'s name):
91
+
92
+ ```console
93
+ $ printf '%s\n' \
94
+ '{"executable": "curl", "args": ["-Lo", "example.com.html", "https://example.com"], "dirname": "example.com"}' \
95
+ '{"executable": "curl", "args": ["-Lo", "example.org.html", "https://example.org"], "dirname": "example.org"}' \
96
+ | penrun -b -J
97
+ ```
98
+
99
+ Run `penrun -h` for the rest of the batch flags (`-S`, `-u`, ...).
100
+
101
+ ## Python API
102
+
103
+ `penrun` exposes the same functionality as an importable library, so other Python tools can drive it
104
+ without going through the CLI.
105
+
106
+ Single command - equivalent to `penrun -t staging curl https://example.com`:
107
+
108
+ ```python
109
+ import penrun
110
+
111
+ options = penrun.RunOptions(config=penrun.Config(artifacts_base="penrun-artifacts"), tag="staging")
112
+ penrun.run(["curl", "https://example.com"], options)
113
+ ```
114
+
115
+ Batch, every job running the same program - equivalent to the `-J` example above:
116
+
117
+ ```python
118
+ batch = penrun.Batch(
119
+ executable="curl",
120
+ shared_args=["-Lo"],
121
+ jobs=[
122
+ penrun.BatchJob(args=["example.com.html", "https://example.com"], dirname="example.com"),
123
+ penrun.BatchJob(args=["example.org.html", "https://example.org"], dirname="example.org"),
124
+ ],
125
+ )
126
+ penrun.run_batched(batch, options, group_by_executable=True, schedule=penrun.ScheduleOptions(max_workers=2))
127
+ ```
128
+
129
+ Batch mixing completely different programs, by overriding `executable` on the jobs that don't run
130
+ `Batch.executable`:
131
+
132
+ ```python
133
+ batch = penrun.Batch(
134
+ executable="nmap",
135
+ jobs=[
136
+ penrun.BatchJob(args=["-oA", "scan", "example.com"], dirname="nmap"),
137
+ penrun.BatchJob(executable="curl", args=["-I", "https://example.com"], dirname="curl-head"),
138
+ ],
139
+ )
140
+ penrun.run_batched(batch, options)
141
+ ```
142
+
143
+ Errors raise `penrun.PenrunError` subclasses (e.g. `penrun.CommandNotFoundError`, `penrun.ConfigError`,
144
+ each carrying a matching `.exit_code`) instead of exiting the process. If `RunOptions.timeout`/`-w` fires,
145
+ COMMAND is killed and Python's built-in `TimeoutError` is raised instead.
146
+
147
+ ## Configuration
148
+
149
+ `penrun` looks for a config file in this order and stops at the first match: `./.penrun.toml`,
150
+ `$(git rev-parse --show-toplevel)/.penrun.toml`, then `~/.config/penrun/config.toml`. See
151
+ [`config.toml`](config.toml) for all available keys with explanations.
152
+
153
+ ## Development
154
+
155
+ ```console
156
+ $ just lint
157
+ $ just fmt
158
+ $ just test
159
+ ```
penrun-0.2.2/README.md ADDED
@@ -0,0 +1,148 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: AISEC Pentesting Team
3
+
4
+ SPDX-License-Identifier: CC0-1.0
5
+ -->
6
+
7
+ # penrun
8
+
9
+ This tool was initially rescued from the gallia [commit history](https://github.com/Fraunhofer-AISEC/gallia/commit/985664905b6ef352db6fdc895af2ac79492539da) and ported to Python.
10
+
11
+ `penrun` runs any command inside a well-defined, timestamped artifacts directory. Point it at a test,
12
+ scan, or anything else you invoke repeatedly, and every run gets its own directory with the command's
13
+ compressed output and exit code - so results are never overwritten and old runs stay reproducible.
14
+
15
+ ## Requirements
16
+
17
+ Python 3.14+, standard library only. No runtime dependencies.
18
+
19
+ ## Installation
20
+
21
+ ```console
22
+ $ uvx penrun -h
23
+ ```
24
+
25
+ Without installing, `uv run penrun` also works straight out of a checkout.
26
+
27
+ ## Quick start: normal mode
28
+
29
+ Run any command; `penrun` creates `penrun-artifacts/<command>/run-<timestamp>/` for it:
30
+
31
+ ```console
32
+ $ penrun ls -lah
33
+ $ tree penrun-artifacts/ls
34
+ penrun-artifacts/ls
35
+ ├── LATEST -> run-20260723-101334.123456
36
+ └── run-20260723-101334.123456
37
+ ├── META.json # command, start/end time, exit code, environment (with -e), error (on failure)
38
+ └── OUTPUT.zst # combined stdout+stderr, zstd-compressed
39
+ ```
40
+
41
+ `LATEST` always points at the most recent run of that command, regardless of tags or where
42
+ `artifacts_template` places the timestamped directory (see below).
43
+
44
+ `penrun` sets these environment variables for COMMAND:
45
+
46
+ - `PENRUN_COMMAND` - the command as run, shell-quoted.
47
+ - `PENRUN_ARTIFACTS` - the absolute path of this run's artifacts directory.
48
+ - `PENRUN_BATCH_MODE=1` - only in batch mode.
49
+ - `PENRUN_PWD_CONF`/`PENRUN_GIT_ROOT_CONF`/`PENRUN_USER_CONF` - path of each config file found while
50
+ searching (see [Configuration](#configuration)), regardless of which one was actually used.
51
+
52
+ Run `penrun -h` for the full list of flags.
53
+
54
+ ## Quick start: batch mode
55
+
56
+ Batch mode (`-b`) reads a command - or command fragment - per line from stdin and runs each one,
57
+ sequentially by default (`-j N` for up to `N` in parallel):
58
+
59
+ ```console
60
+ $ printf 'true\nfalse\ntrue\n' | penrun -b
61
+ ```
62
+
63
+ Pass `-T` with a template to turn plain input lines into full commands; `{1}` is replaced with the
64
+ line. Since every job below runs `curl`, add `-N` to name each job's directory after its input line
65
+ instead of the program name - otherwise they'd all land in the same `curl/` directory:
66
+
67
+ ```console
68
+ $ printf 'example.com\nexample.org\n' | penrun -b -N -T 'curl -Lo {1}.html https://{1}'
69
+ $ tree -L 2 penrun-artifacts/curl
70
+ penrun-artifacts/curl
71
+ ├── LATEST -> run-20260723-101334.123456
72
+ └── run-20260723-101334.123456
73
+ ├── example.com
74
+ └── example.org
75
+ ```
76
+
77
+ For programmatic callers that already know each job's command and artifacts directory name, `-J`/`--json`
78
+ reads newline-delimited JSON objects instead of plain lines - one `{"executable": "...", "args": [...],
79
+ "dirname": "..."}` per line (`dirname` is optional, falling back to `executable`'s name):
80
+
81
+ ```console
82
+ $ printf '%s\n' \
83
+ '{"executable": "curl", "args": ["-Lo", "example.com.html", "https://example.com"], "dirname": "example.com"}' \
84
+ '{"executable": "curl", "args": ["-Lo", "example.org.html", "https://example.org"], "dirname": "example.org"}' \
85
+ | penrun -b -J
86
+ ```
87
+
88
+ Run `penrun -h` for the rest of the batch flags (`-S`, `-u`, ...).
89
+
90
+ ## Python API
91
+
92
+ `penrun` exposes the same functionality as an importable library, so other Python tools can drive it
93
+ without going through the CLI.
94
+
95
+ Single command - equivalent to `penrun -t staging curl https://example.com`:
96
+
97
+ ```python
98
+ import penrun
99
+
100
+ options = penrun.RunOptions(config=penrun.Config(artifacts_base="penrun-artifacts"), tag="staging")
101
+ penrun.run(["curl", "https://example.com"], options)
102
+ ```
103
+
104
+ Batch, every job running the same program - equivalent to the `-J` example above:
105
+
106
+ ```python
107
+ batch = penrun.Batch(
108
+ executable="curl",
109
+ shared_args=["-Lo"],
110
+ jobs=[
111
+ penrun.BatchJob(args=["example.com.html", "https://example.com"], dirname="example.com"),
112
+ penrun.BatchJob(args=["example.org.html", "https://example.org"], dirname="example.org"),
113
+ ],
114
+ )
115
+ penrun.run_batched(batch, options, group_by_executable=True, schedule=penrun.ScheduleOptions(max_workers=2))
116
+ ```
117
+
118
+ Batch mixing completely different programs, by overriding `executable` on the jobs that don't run
119
+ `Batch.executable`:
120
+
121
+ ```python
122
+ batch = penrun.Batch(
123
+ executable="nmap",
124
+ jobs=[
125
+ penrun.BatchJob(args=["-oA", "scan", "example.com"], dirname="nmap"),
126
+ penrun.BatchJob(executable="curl", args=["-I", "https://example.com"], dirname="curl-head"),
127
+ ],
128
+ )
129
+ penrun.run_batched(batch, options)
130
+ ```
131
+
132
+ Errors raise `penrun.PenrunError` subclasses (e.g. `penrun.CommandNotFoundError`, `penrun.ConfigError`,
133
+ each carrying a matching `.exit_code`) instead of exiting the process. If `RunOptions.timeout`/`-w` fires,
134
+ COMMAND is killed and Python's built-in `TimeoutError` is raised instead.
135
+
136
+ ## Configuration
137
+
138
+ `penrun` looks for a config file in this order and stops at the first match: `./.penrun.toml`,
139
+ `$(git rev-parse --show-toplevel)/.penrun.toml`, then `~/.config/penrun/config.toml`. See
140
+ [`config.toml`](config.toml) for all available keys with explanations.
141
+
142
+ ## Development
143
+
144
+ ```console
145
+ $ just lint
146
+ $ just fmt
147
+ $ just test
148
+ ```
@@ -8,8 +8,9 @@ build-backend = "uv_build"
8
8
 
9
9
  [project]
10
10
  name = "penrun"
11
- version = "0.2.1"
11
+ version = "0.2.2"
12
12
  description = "Run any command inside a well-defined, timestamped artifacts directory."
13
+ readme = "README.md"
13
14
  requires-python = ">=3.14"
14
15
  license = { text = "Apache-2.0" }
15
16
  authors = [{ name = "AISEC Pentesting Team" }]
penrun-0.2.1/PKG-INFO DELETED
@@ -1,9 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: penrun
3
- Version: 0.2.1
4
- Summary: Run any command inside a well-defined, timestamped artifacts directory.
5
- Author: AISEC Pentesting Team
6
- License: Apache-2.0
7
- Maintainer: Stefan Tatschner, Tobias Specht
8
- Maintainer-email: Stefan Tatschner <stefan.tatschner@aisec.fraunhofer.de>, Tobias Specht <tobias.specht@aisec.fraunhofer.de>
9
- Requires-Python: >=3.14
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes