ppkit 0.1.0__tar.gz → 0.2.1__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.
Files changed (65) hide show
  1. ppkit-0.2.1/CHANGELOG.md +346 -0
  2. {ppkit-0.1.0 → ppkit-0.2.1}/CONTRIBUTING.md +98 -26
  3. {ppkit-0.1.0 → ppkit-0.2.1}/PKG-INFO +65 -22
  4. {ppkit-0.1.0 → ppkit-0.2.1}/README.md +63 -20
  5. {ppkit-0.1.0 → ppkit-0.2.1}/bin/pp +4 -2
  6. {ppkit-0.1.0 → ppkit-0.2.1}/ci/check +2 -2
  7. {ppkit-0.1.0 → ppkit-0.2.1}/pp/__init__.py +1 -1
  8. {ppkit-0.1.0 → ppkit-0.2.1}/pp/cli.py +158 -22
  9. {ppkit-0.1.0 → ppkit-0.2.1}/pp/commands/batch.py +299 -31
  10. {ppkit-0.1.0 → ppkit-0.2.1}/pp/commands/cmd.py +288 -45
  11. ppkit-0.2.1/pp/commands/completion.py +66 -0
  12. {ppkit-0.1.0 → ppkit-0.2.1}/pp/commands/http.py +36 -12
  13. {ppkit-0.1.0 → ppkit-0.2.1}/pp/commands/kill.py +23 -3
  14. {ppkit-0.1.0 → ppkit-0.2.1}/pp/commands/promote.py +402 -77
  15. ppkit-0.2.1/pp/commands/queue.py +449 -0
  16. {ppkit-0.1.0 → ppkit-0.2.1}/pp/lib/config.py +10 -15
  17. ppkit-0.2.1/pp/lib/jobcore.py +913 -0
  18. ppkit-0.2.1/pp/lib/jobpolicy.py +1222 -0
  19. ppkit-0.2.1/pp/lib/jobq.py +2211 -0
  20. ppkit-0.2.1/pp/lib/jobstore.py +486 -0
  21. {ppkit-0.1.0 → ppkit-0.2.1}/pp/lib/paths.py +14 -8
  22. {ppkit-0.1.0 → ppkit-0.2.1}/pp/lib/proc.py +1 -1
  23. ppkit-0.2.1/pp/lib/session.py +97 -0
  24. {ppkit-0.1.0 → ppkit-0.2.1}/pp/lib/ui.py +101 -8
  25. {ppkit-0.1.0 → ppkit-0.2.1}/pyproject.toml +2 -2
  26. {ppkit-0.1.0 → ppkit-0.2.1}/tests/journey.py +21 -1
  27. {ppkit-0.1.0 → ppkit-0.2.1}/tests/sandbox.py +5 -1
  28. {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_batch.py +200 -0
  29. ppkit-0.2.1/tests/test_cli.py +283 -0
  30. {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_cmd.py +55 -0
  31. {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_constructs.py +463 -172
  32. {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_http.py +98 -0
  33. {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_journey_batch.py +25 -0
  34. {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_journey_cli.py +175 -9
  35. {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_journey_cmd.py +185 -5
  36. {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_journey_http.py +2 -0
  37. {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_journey_kill.py +4 -1
  38. {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_journey_promote.py +18 -0
  39. ppkit-0.2.1/tests/test_journey_queue.py +1530 -0
  40. {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_kill.py +31 -0
  41. ppkit-0.2.1/tests/test_promote.py +1336 -0
  42. ppkit-0.2.1/tests/test_queue.py +6271 -0
  43. ppkit-0.2.1/tests/test_session.py +65 -0
  44. ppkit-0.2.1/tests/test_ui.py +319 -0
  45. ppkit-0.1.0/CHANGELOG.md +0 -20
  46. ppkit-0.1.0/pp/commands/completion.py +0 -55
  47. ppkit-0.1.0/pp/commands/queue.py +0 -243
  48. ppkit-0.1.0/pp/lib/jobq.py +0 -3106
  49. ppkit-0.1.0/pp/lib/session.py +0 -31
  50. ppkit-0.1.0/tests/test_cli.py +0 -166
  51. ppkit-0.1.0/tests/test_journey_queue.py +0 -637
  52. ppkit-0.1.0/tests/test_promote.py +0 -679
  53. ppkit-0.1.0/tests/test_queue.py +0 -3280
  54. ppkit-0.1.0/tests/test_session.py +0 -24
  55. ppkit-0.1.0/tests/test_ui.py +0 -107
  56. {ppkit-0.1.0 → ppkit-0.2.1}/.pp.toml +0 -0
  57. {ppkit-0.1.0 → ppkit-0.2.1}/LICENSE +0 -0
  58. {ppkit-0.1.0 → ppkit-0.2.1}/ci/package +0 -0
  59. {ppkit-0.1.0 → ppkit-0.2.1}/pp/__main__.py +0 -0
  60. {ppkit-0.1.0 → ppkit-0.2.1}/pp/commands/__init__.py +0 -0
  61. {ppkit-0.1.0 → ppkit-0.2.1}/pp/lib/__init__.py +0 -0
  62. {ppkit-0.1.0 → ppkit-0.2.1}/pp/lib/lock.py +0 -0
  63. {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_lock.py +0 -0
  64. {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_paths.py +0 -0
  65. {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_proc.py +0 -0
@@ -0,0 +1,346 @@
1
+ # Changelog
2
+
3
+ ## v0.2.1
4
+
5
+ pp finds the session that runs it with nothing set, so `--mine` works for
6
+ any agent and in a terminal. `pp queue` reads the cards with a command of
7
+ yours, and any command answers to an alias of your own. A `pp-NAME` program
8
+ is a command only in pp's own folders, and no longer on your `PATH`.
9
+
10
+ `pp`: a program named `pp-NAME` on your `PATH` is no longer a command.
11
+
12
+ - pp looks for such a program in `~/.config/pp/commands/`, in each plugin
13
+ and in each `PP_PATH` folder, and reads no `PATH` for one. Other software
14
+ installs programs named `pp-*`, and on `PATH` such a program takes a
15
+ command's name or a unique prefix of it (`pp q`) and shows in `pp -h`. A
16
+ program of yours stays a command by moving it into
17
+ `~/.config/pp/commands/`, or by listing the folder it is in in `PP_PATH`.
18
+ - Every Python command is still found before any such program, and the
19
+ programs in that folder order, so nothing pp found in those folders
20
+ moves.
21
+
22
+ `pp queue`: the card reading and the calling session are named by you, with
23
+ nvidia-smi and the process pp runs under as the defaults.
24
+
25
+ - `[resources] gpu_gb = { probe = "CMD" }` reads the machine's cards with a
26
+ command of yours instead of nvidia-smi. CMD prints one line a card,
27
+ `index,name,total_MiB,used_MiB,util`, which is nvidia-smi's own
28
+ `--format=csv,noheader,nounits` line, and the index it prints is the one pp
29
+ reserves and publishes in `CUDA_VISIBLE_DEVICES`. The reading is cached for
30
+ the pool's `every` and taken under the probe's own claim, as any probe's is.
31
+ A probe in a `[queue.NAME.resources] gpu_gb` is still refused: one machine
32
+ has one set of cards.
33
+ - `pp queue status --json` gains `cards_why`: why `cards` is null, and `""`
34
+ where the cards were read.
35
+ - pp finds the calling session from the process it runs under: the nearest
36
+ process above pp that is neither a shell nor a wrapper, named with its pid,
37
+ its start time and this boot. So `--mine` and a job's `owner` are any
38
+ agent's with nothing set, since every command an agent runs is a child of
39
+ the agent's own process, and a person's terminal is a session the same way.
40
+ `PP_SESSION`, where it is set and not empty, is the name an agent gives its
41
+ own session instead, and wins over `CLAUDE_CODE_SESSION_ID`. A process pp
42
+ started itself, a job or its runner or a hook or a stop command, is no
43
+ session. pid 1 and an init are no session either, so a `wait` left running
44
+ by a shell that has returned, which the kernel reparents to an init, says
45
+ there is none instead of answering for another session's jobs. The session
46
+ is found once in a run, so such a wait reports the outcomes it selected to
47
+ the session that queued them and no later `wait --mine` of that session
48
+ reports them again. pp strips those variables from everything it starts.
49
+ - `pp queue status --mine`, which `pp queue --mine` and `ls --mine` are too,
50
+ is the jobs this session queued, in every state, in the text view and in
51
+ `--json`'s `running`, `queued`, `held` and `recent`. It applies together
52
+ with `--here` and `--project`, and narrows the jobs shown and not what the
53
+ queues, the workers and the pools hold. Where there is no session it exits
54
+ 2, as `wait --mine` does.
55
+
56
+ `pp`: a command answers to a name of your own.
57
+
58
+ - `[alias]` in `~/.config/pp/config.toml` names any command anything on this
59
+ machine: `st = "promote status"`, or a list of words. A word is resolved
60
+ as a command's own name, then `[alias]`, then a module's `ALIASES`, then a
61
+ unique prefix, so an alias hides no command and beats a prefix. `pp help
62
+ st` and `pp st <TAB>` follow the expansion.
63
+ - An alias runs no other alias. One whose key is a command's name or `help`,
64
+ or outside `[a-z][a-z0-9_-]*`, or whose first word names no command, is
65
+ skipped and listed under `pp -h`'s "broken (skipped)". The table is read
66
+ only where a word is not a command's exact name, so a config file that
67
+ cannot be parsed leaves every command pp ships working.
68
+ - `pp cmd alias` lists every alias of both kinds, with what it runs and
69
+ where it is declared (`--json` for the same as data). `pp cmd alias NAME
70
+ CMD [ARGS...]` adds one, `pp cmd alias NAME --rm` takes it back, and `-n`
71
+ prints the change and writes nothing. Those flags count wherever they
72
+ stand, so `pp cmd alias st promote status -n` is a dry run. A command line
73
+ of its own `-n`, `--rm` or `--json` goes after a `--`: `pp cmd alias dq --
74
+ queue drop -n`.
75
+ - `pp cmd alias` is the one place pp writes `config.toml`: one line of the
76
+ `[alias]` table under a lock, the file's mode kept, every other byte as it
77
+ was. A change it cannot make as one line names the file to edit by hand.
78
+ - `pp cmd ls` shows each command's aliases of both kinds, and its `--json`
79
+ rows gain `config_aliases` beside `aliases`.
80
+
81
+ `pp queue`: the views, the plans and the help say more of what pp knows
82
+ about your cards, your queues and your projects.
83
+
84
+ - `status --json` publishes the gpu_gb pool of each scope in `pools` and
85
+ `shared`, an entry like every other pool's with one field of its own:
86
+ `per_card`, that scope's budget on each card of the reading and what its
87
+ running jobs reserve on that card. The text view shows those numbers card
88
+ by card. So a job is sized against the smallest budget of every scope its
89
+ queue draws from before it is queued, instead of being refused first.
90
+ - `pp queue` covers every queue a `[queue.NAME]` table names, beside the
91
+ queues that hold records and the ones a worker is up for. Such a queue
92
+ with nothing in it is a section with nothing in it and an entry in
93
+ `--json`, and reading it creates no directory and no file. "no queues yet"
94
+ is for a machine with none of the three.
95
+ - `pp queue add` warns on stderr where the job has no runner, no `.pp.toml`
96
+ is in or above its directory, and a directory directly below is a project
97
+ whose `[queue]` sets one. The warning names that directory and `--cwd`,
98
+ once per directory, and the job is queued as it asks.
99
+ - The command `add` hands an agent names the ids it queued as ranges (`wait
100
+ 8-13 20`), which is one word for a campaign of a hundred lines.
101
+ - `clean` and `drop` say in their plan that they remove the records and pp's
102
+ own files for them, and that a log at a path a `log` pattern or `--log`
103
+ named stays.
104
+ - The extra run of the project's stop command once a run is over writes to
105
+ the worker's log instead of the job's log. The stop a kill or a timeout
106
+ asked for still writes in the job's log.
107
+ - `pp help queue` names the shape of each `--json`: one object with its keys
108
+ for `status`, one job and its watchers for `show`, and a list of `{queue,
109
+ pid, how, log, up}` for `worker status`.
110
+ - `pp help queue` says that several selection forms are several words on one
111
+ verb, that a tag or a prefix selects every job of that name the store
112
+ holds, an earlier round's ended ones too, and that a campaign line's
113
+ `--cwd` is that line's own directory and so its project.
114
+
115
+ ## v0.2.0
116
+
117
+ The interface this minor version keeps. `pp help queue` says what is promised
118
+ and what may change in a patch. `pp queue` stops what a container runner
119
+ started, takes a campaign from a file with one job's flags per line, and runs
120
+ a stage after the one it names.
121
+
122
+ `pp queue`: a job is named and selected the same way by every verb.
123
+
124
+ - A job added without `--tag` has no name. Views show its command, and a
125
+ selection by tag never matches it.
126
+ - A `--tag` is at most 64 characters of letters, digits and `.`, `_`, `-`
127
+ and `:`. It may not hold `..`, be `.` on its own, or read as an id or a
128
+ range, because a tag can be part of a log file's name.
129
+ - `counter` is not a queue name, beside `probes` and `watchers`: a queue of
130
+ that name took the place of the job id counter, and every later `add`
131
+ failed.
132
+ - Every verb that takes jobs takes the same five forms: an id, a range
133
+ (`12-15`), a tag matched whole, `--prefix P` (repeatable), and `--all`
134
+ with `-q NAME`. `--all` without `-q` is a usage error, and a tag or a
135
+ prefix that names jobs in two queues is an error that lists the ids.
136
+ `pp queue note` is the exception, and takes one job by its id or by a
137
+ tag that names one.
138
+ - `pp queue clean` takes a selection as well as `--older AGE`.
139
+
140
+ `pp queue`: a project says how its work is launched and stopped, so a kill
141
+ and a timeout reach work that is not in the job's process group.
142
+
143
+ - `[queue] stop` in a project's `.pp.toml`, and `--stop CMD` on `add`:
144
+ `"group"` where the runner's work stays in the job's own process group,
145
+ else a shell command that ends it. `pp queue add` is refused in a project
146
+ that sets `[queue] runner` and no stop, naming both spellings.
147
+ `--runner CMD` on `add` sets the runner for one job.
148
+ - In a project with a command stop, `pp queue kill` and a `--timeout` run
149
+ that command in place of the SIGTERM to the job's process group. The
150
+ grace, the SIGKILL and the sweep of the group are unchanged, so a project
151
+ with `stop = "group"` behaves as it did. The kill line says which ran, and
152
+ a stop command that failed is reported with its exit code.
153
+ - A job added with `--no-runner` carries `stop = "group"` whatever its
154
+ project says.
155
+ - The sweep that runs when a job's run is over also runs the project's stop
156
+ command, where there is one and the attempt wrote no exit code of its own,
157
+ and it takes the job's process group with it. So nothing the runner
158
+ started outlives the attempt that started it, and a leaked process no
159
+ longer holds a reservation the next job is told it may use.
160
+ - A job and its runner are given `PP_QUEUE_CONTAINER`, a name no other run
161
+ on the machine has, one per attempt, beginning `pp-<queue>-<id>-`; and
162
+ `PP_QUEUE_ENVFILE`, the path of a file of `KEY=VALUE` lines readable by
163
+ its owner alone. That file holds the project's `[queue] env`, the job's
164
+ `--env`, `CUDA_VISIBLE_DEVICES` where a card was reserved, `PP_QUEUE`,
165
+ `PP_QUEUE_JOB` and `PP_QUEUE_CONTAINER`, and nothing else of the
166
+ machine's: no `PATH`, `VIRTUAL_ENV`, `CONDA_PREFIX`, `CONDA_DEFAULT_ENV`
167
+ or `PP_QUEUE_RCFILE`. The record names it, and it goes with the record.
168
+ - An `--env` value, or a `[queue] env` value, holding a newline is refused,
169
+ and so is a name holding `=`, NUL or a newline.
170
+ - An exit code an attempt wrote while a stop of it was going out is that
171
+ stop's outcome, killed or timeout, with the code kept as `rc`.
172
+ - `pp queue kill --grace DUR` takes a duration and overrides the job's own
173
+ grace for the jobs that kill names.
174
+
175
+ `pp queue`: a job's output goes where the project says.
176
+
177
+ - `log` in a project's `[queue]`, and `--log PATH` on `add`: a path with
178
+ `{tag}` or `{id}` in it, relative to the project root or absolute. Any
179
+ other placeholder, a path holding `..`, and a relative path that leaves
180
+ the project root are refused. `{tag}` on a job with no tag is filled in
181
+ with the job's id.
182
+ - Every attempt appends to that one file, after a `[pp queue]` line naming
183
+ the job and its command, and several jobs may name one file. The record
184
+ names the file; `log`, `show`, a failure's tail and `PP_QUEUE_LOG` all
185
+ read it from there. `drop` and `clean` remove the files pp put in its own
186
+ store, and never a log at a path you named.
187
+ - `--retry-if` matches from the byte offset where the failed attempt's own
188
+ output begins, so it no longer matches an earlier attempt's lines or pp's
189
+ own line.
190
+
191
+ `pp queue`: a campaign file carries one job's flags per line, and a job can
192
+ wait for the jobs it names.
193
+
194
+ - A line of a `--from` file is the rest of an `add` command line: its own
195
+ flags, then `--`, then the command. A flag on the `add` is the default for
196
+ every line and a line's own flag wins over it; `--env` and `--need` merge
197
+ key by key. A line that is only a command behaves as before.
198
+ - A line is split into words the way a shell splits them, with quotes and
199
+ backslashes honoured. A line holding `|`, `&`, `;`, `<`, `>`, `(`, `)`,
200
+ `` ` `` or `$(` outside quotes is refused with its line number, and so is a
201
+ line with no `--` whose first word begins with `-`. A file one line of
202
+ which is refused queues nothing. A command that wants a shell operator
203
+ passes it quoted: `-- sh -c 'python x.py | grep -v noise'`.
204
+ - `--from FILE` together with a command on the same command line is a usage
205
+ error, and so is a line that names the queue, the file, `--hold` or
206
+ `--no-start`, which are the batch's and no line's.
207
+ - `--after SELECTION` on `add`: the job waits until every job that selection
208
+ named has ended, whatever the outcome. The value is one or more selections
209
+ separated by commas (`41,42`, `41-66`, `s1_eval,s1_report`), read as the
210
+ job is added and kept as ids on the record. An id no job has is a usage
211
+ error and nothing is queued. A job that is waiting is skipped rather than
212
+ waited for, so it never becomes the head of its queue, and a job whose
213
+ record is gone counts as ended.
214
+ - `pp queue retry` over a range, a tag, a prefix or `--all` shows a plan and
215
+ asks, as `kill` and `drop` do; `-n` shows the plan and `-y` does not ask.
216
+ A job that succeeded has ended, so it suits `retry`. The new job is owned
217
+ by the session that asked for it, and a priority is set with
218
+ `pp queue reprio` on the job the retry queued.
219
+ - A bare `pp queue wait` waits for every job that is queued or running
220
+ instead of returning at the first outcome. It names no held job; a
221
+ selection that names one waits for it and says so. `--timeout` is honoured
222
+ whatever `--poll` is, and ids together with `--idle` or `--mine` is a
223
+ usage error.
224
+ - A worker that adopts a running job takes the offset its output begins at
225
+ from the job's own run claim, so `--retry-if` reads that attempt's lines
226
+ and never pp's own line.
227
+
228
+ `pp queue`: every setting has one declaration, one default and one order of
229
+ its sources.
230
+
231
+ - Each setting of a job is a key of a project's `.pp.toml [queue]` as well
232
+ as of `[queue.NAME]`: `timeout`, `retries`, `retry_delay`, `retry_if`,
233
+ `on_fail`, `on_done`, `prio`, `gpu_gb`, `cpu`, `mem_gb`, `need`, `env`,
234
+ `log` and `grace`. The first source that gives a setting wins, in this
235
+ order: `add`'s flag, the project's `[queue]`, the queue's `[queue.NAME]`,
236
+ the built-in default. `env` and `need` merge key by key.
237
+ - `grace` is one setting of the job: `--grace DUR` on `add`, the `grace`
238
+ key, the resolved value on the record, and 10s by default. A timeout's
239
+ stop and a kill of that job both take it.
240
+ - Durations are read one way everywhere: `90s`, `30m`, `2h`, `1d`, a bare
241
+ number of seconds, `.5` and `1.5h`, and `none` where a setting offers it.
242
+ An empty value, a value that is not finite and one over 1e9 seconds are
243
+ refused, in a config key as on a command line.
244
+ - Numbers are bounded: `retries` at most 1000, `retry_delay` at most the
245
+ one-hour cap, a queue's `poll` longer than 0. A need that is 0, negative
246
+ or nan is refused.
247
+ - `pp queue work` takes no `--backfill` and no `--poll`: both are keys of
248
+ `[queue.NAME]`, which the queue's worker re-reads every loop, so they
249
+ change under running jobs with no restart.
250
+
251
+ `pp queue`: the store and the machine surface.
252
+
253
+ - A queue's `FORMAT` is 2. A pp that reads another format refuses the queue
254
+ where it opens it, naming the format the queue says and the one that pp
255
+ reads, rather than reading it.
256
+ - The `--json` field `gpu`, an integer, becomes `cards`, a list of card
257
+ indices; a job that reserved no card has `cards: []`.
258
+ - New `--json` fields: `stop_cmd`, `grace`, `after`, `container`, `envfile`,
259
+ `log_at`, and `cards`, `container`, `envfile` and `log_at` in each
260
+ `history` entry. `show --json` carries every promised field on a job in
261
+ each of the five states: a field the record does not hold yet is null, or
262
+ an empty list. The two exceptions are `after`, which is there only on a
263
+ job that names the jobs it waits for, and `stop`, only on a job asked to
264
+ stop.
265
+ - `pp help queue` is one page: a first job, a project whose work runs in a
266
+ container in four steps, names and selection, campaign files, the settings
267
+ table, how a job ends, workers, and what is left to you.
268
+
269
+ `pp kill`: `--grace DUR` takes a duration in the spelling every duration pp
270
+ takes has. A bare number is still seconds.
271
+
272
+ `pp promote`, `pp cmd` and `pp http`: plans under `-n`, help by flag, and
273
+ links one target holds.
274
+
275
+ - `pp -h NAME` and `pp --help NAME` print that command's help, as
276
+ `pp help NAME` does. `pp -h NAME VERB` prints the verb's.
277
+ - `pp promote rollback`, `pp promote link`, `pp promote init` and
278
+ `pp cmd new` take `-n`: each one shows its plan and changes nothing.
279
+ - `pp promote` does not take over a link path another target holds: the
280
+ refusal names that target, and `--force-links` moves the link.
281
+ - `pp promote status --json` has `failed`: the current release's
282
+ activation failed, or nothing gated it.
283
+ - `pp promote diff --json` has `source_gone`. A target whose project
284
+ directory is gone exits 1 there, in place of git's error.
285
+ - `pp cmd ls --json` has `aliases`, each command's short names.
286
+ - An unknown key in `[[http.roots]]` or `[[http.discover]]` is a config
287
+ error naming the file and the key, so a misspelt `marker` stops the
288
+ server instead of serving every folder under `under`.
289
+ - `pp help http` documents every `[http]` key with its default.
290
+
291
+ ## v0.1.1
292
+
293
+ Fixes. A few command lines and config values that did harm are refused.
294
+
295
+ - `pp batch run` refuses a file-name field where a shell would read the
296
+ name again as code: inside `$( )`, backticks, `${ }`, arithmetic, a
297
+ `[[ ]]` test, a case pattern, a comment, `eval`, `trap`, a shell's `-c`
298
+ script, or a template with a heredoc or a function. The message names the
299
+ spelling that works: `sh -c 'cmd "$1"' sh {}`. Its plan lists every
300
+ command.
301
+ - A `pp batch rename` that is interrupted leaves a journal of what landed,
302
+ and `pp batch undo` takes that journal and says so.
303
+ - `pp promote WORD` releases nothing when the word is neither a verb nor a
304
+ path. It used to release the project in the current directory.
305
+ - A wrong value in `[promote]` is refused, naming the file and the key.
306
+ `name` must be one path component: `name = ".."` let `gc` remove every
307
+ release and plugin.
308
+ - `pp promote` keeps the release it flips away from, so the one you rolled
309
+ back to is still there after the next promote.
310
+ - A socket or fifo in a tree project no longer hangs `pp promote`, and a
311
+ file pp cannot read is a blocker that offers `exclude`.
312
+ - A hook that leaves a process running no longer holds `pp promote`. Each
313
+ hook's output is a file beside the run's log.
314
+ - A release that `pp promote gc` cannot remove stays whole, and gc exits 1.
315
+ - `pp queue`: a job that cannot start fails on its own and the rest start.
316
+ A job whose wrapper is still ending reads running, not lost.
317
+ - `pp queue --json` and `pp queue show --json` give a job's variables by
318
+ name, with each value `***`.
319
+ - An empty duration such as `--timeout ""` is a usage error, and so is an
320
+ empty `pp kill` target, which selected every process you own.
321
+ - `pp http` never serves `.git`, serves a file whose name is not UTF-8, and
322
+ sorts by name when `?sort=` names anything else.
323
+ - pp's messages and the `pp cmd add` and `update` screens escape the text
324
+ they name. TAB completion runs nothing in a command's name: run
325
+ `pp completion bash` or `zsh` again to replace a saved script.
326
+ - A command file whose name is not UTF-8 no longer breaks `pp -h`.
327
+ - pp needs Python 3.11.4 or later, which `pp promote` always needed.
328
+
329
+ ## v0.1.0
330
+
331
+ The first release.
332
+
333
+ - `pp queue` runs commands later, in named queues, when the GPU memory,
334
+ CPUs, RAM or counters they ask for are free. Jobs have timeouts, retries
335
+ for the failures you name, and hooks. Tags and ranges select groups of
336
+ jobs. A project's `.pp.toml` can run its jobs through a runner, such as a
337
+ container.
338
+ - `pp queue wait`, `watch` and `wait --mine` tell a person or an agent how
339
+ jobs ended, with the end of the log for a failure.
340
+ - `pp promote` releases a project from a git commit when its check passes
341
+ on that copy, and rolls back.
342
+ - `pp kill`, `pp batch` and `pp http` stop processes, rename or convert
343
+ files, and show project folders in a browser.
344
+ - `pp cmd` writes commands, and adds other people's from git, pinned to the
345
+ commit you read.
346
+ - Completion for zsh and bash.
@@ -28,19 +28,19 @@ If you installed pp as a tool, `uv tool uninstall ppkit` (or
28
28
 
29
29
  ## Building and testing
30
30
 
31
- You need Python 3.11 or later, git, sh and bash. The tests that need zsh
31
+ You need Python 3.11.4 or later, git, sh and bash. The tests that need zsh
32
32
  skip without it. `ci/check` uses [uv](https://docs.astral.sh/uv/)
33
33
  for the linter and for other Python versions.
34
34
 
35
35
  ```
36
36
  ci/check the linter, then every test, on python3
37
- ci/check 3.11 the same on Python 3.11, which uv installs the first time
38
- ci/check --cpus 2 3.11 the same on 2 CPUs, like a CI runner
37
+ ci/check 3.11.4 the same on Python 3.11.4, which uv installs the first time
38
+ ci/check --cpus 2 3.11.4 the same on 2 CPUs, like a CI runner
39
39
  python3 -m unittest discover -s tests the tests alone
40
40
  ci/package build ppkit into dist/, install it fresh, and try it
41
41
  ```
42
42
 
43
- CI runs `ci/check 3.11`, `ci/check 3.14` and `ci/package` on every push to
43
+ CI runs `ci/check 3.11.4`, `ci/check 3.14` and `ci/package` on every push to
44
44
  main and every pull request. The whole suite takes a few minutes. Most of
45
45
  that is the journeys (`tests/test_journey_*.py`), which run the real
46
46
  `bin/pp` once per step in a throwaway home directory.
@@ -55,7 +55,7 @@ on a developer's machine takes tens of milliseconds, and a runner's few
55
55
  hundred take a few. The test that needs another user's process is skipped
56
56
  there.
57
57
 
58
- A uv-managed CPython has no `os.pidfd_open`, so `ci/check 3.11`, `ci/check
58
+ A uv-managed CPython has no `os.pidfd_open`, so `ci/check 3.11.4`, `ci/check
59
59
  3.14` and CI run `pp/lib/proc.py`'s ctypes syscall path, while the distro
60
60
  `python3` of a plain `ci/check` runs the wrapped calls. Both are tested only
61
61
  by running both.
@@ -87,7 +87,9 @@ by running both.
87
87
  2. `def main(argv: list[str]) -> int`, which builds its parser with
88
88
  `ui.parser("NAME", __doc__)`.
89
89
  3. If you want short names for it, `ALIASES = ["x"]`, written as a literal
90
- list.
90
+ list. A name one machine wants is `[alias]` in its own `config.toml`
91
+ instead (`pp cmd alias x NAME`), which beats both `ALIASES` and a
92
+ prefix and is checked against the command names before it is written.
91
93
 
92
94
  These rules keep pp working as commands come and go:
93
95
 
@@ -149,6 +151,13 @@ These rules keep pp working as commands come and go:
149
151
  command, two workers, a signal and a write) go through a construct that
150
152
  is written once. "The constructs" below is the list of them, and a check
151
153
  added at one call site is not a fix.
154
+ - A test checks a contract, not a wording. Every sentence pp prints may be
155
+ reworded in a patch, so a test asserts an exit status, a `--json` field, a
156
+ record field, a file's existence or its mode, an id, or a state, and nothing
157
+ else. Where a test has to know that pp said something, it asserts the field
158
+ the sentence was made from. Two exceptions are written down where they are
159
+ made: a journey types the examples of `pp help NAME`, and one journey step
160
+ holds the headings and the line width of `pp help queue`.
152
161
  - A test of a construct checks what the construct promises. A test that
153
162
  runs a second actor at an exact step keeps one fix from coming back
154
163
  (`TestTwoAtOnce` in `tests/test_queue.py`). That step goes through
@@ -176,7 +185,9 @@ These rules keep pp working as commands come and go:
176
185
  | 3 | `pp queue wait` or `watch` gave up at its `--timeout` |
177
186
  | 130 | interrupted (Ctrl-C) |
178
187
 
179
- A `pp-NAME` program's own exit status passes through unchanged.
188
+ A `pp-NAME` program in `~/.config/pp/commands/`, in a plugin or in a
189
+ `PP_PATH` folder is a command, and its own exit status passes through
190
+ unchanged. `PATH` is not read for one.
180
191
 
181
192
  ## How pp works
182
193
 
@@ -185,8 +196,11 @@ list of the parts:
185
196
 
186
197
  - `pp/cli.py`: finding commands, the order of their origins, help and
187
198
  completion.
188
- - `pp/lib/jobq.py`: queues, workers, resources, and the files a job lives
189
- in; `pp help queue` is the user's view of the same.
199
+ - `pp/lib/jobstore.py`: a job's record, the files it lives in, and the
200
+ transaction that changes one. `pp/lib/jobcore.py`: the worker, the claims
201
+ and the processes of a queue. `pp/lib/jobpolicy.py`: every rule of a queue
202
+ as a value, so each one is a table. `pp/lib/jobq.py`: the verbs, the views,
203
+ the resources and the CLI; `pp help queue` is the user's view of the same.
190
204
  - `pp/commands/promote.py`: releases. `pp/commands/cmd.py`: plugins.
191
205
  `pp/commands/http.py`: serving a folder to a browser, with its cache and
192
206
  its server. `pp/commands/batch.py`: bulk file changes, and the journal
@@ -214,9 +228,9 @@ this table, and each rule's own docstring is its contract.
214
228
  | `alive` | judging a process alive by its pid | a claim's holder, or `Handle` | `pp/lib/proc.py` |
215
229
  | `lock` | taking a lock | `locked`, and `deferring` for a scope with no lock | `pp/lib/lock.py` |
216
230
  | `claim` | opening a claim's file | `Claim` | `pp/lib/lock.py` |
217
- | `job` | reading or writing a job's file | `txn`, `listing`, `find` | `pp/lib/jobq.py` |
218
- | `worker-claim` | taking a queue's worker claim, or reading its holder | `worker`, `worker_now`, `work` | `pp/lib/jobq.py` |
219
- | `job-process` | signalling or reaping a job's processes | the `Worker` class, `cmd_worker`, `work` | `pp/lib/jobq.py`, `pp/lib/proc.py`, `pp/commands/kill.py` |
231
+ | `job` | reading or writing a job's file | `txn`, `listing`, `find` | `pp/lib/jobstore.py` |
232
+ | `worker-claim` | taking a queue's worker claim, or reading its holder | `worker`, `worker_now`, `work` | `pp/lib/jobcore.py` |
233
+ | `job-process` | signalling or reaping a job's processes | the `Worker` class, `worker_stop`, `work` | `pp/lib/jobcore.py`, `pp/lib/proc.py`, `pp/commands/kill.py` |
220
234
  | `by-name` | deciding on a served file by its name | act on the open file | `pp/commands/http.py` |
221
235
  | `sleep` | sleeping in a test | `until` | `tests/journey.py` |
222
236
  | `once` | a once-flag of a test's own | `Gate` | `tests/journey.py` |
@@ -246,15 +260,66 @@ replacing a whole file in one step goes through `paths.write_json`.
246
260
  `tests/test_cmd.py` and `ci/package` keep the installed case working, and
247
261
  the journeys in `tests/test_journey_cmd.py` the other two.
248
262
 
263
+ ### The queue's four modules, and what may go where
264
+
265
+ The line between them is mechanism against policy, and a change goes to the
266
+ module whose job it is:
267
+
268
+ - `pp/lib/jobstore.py` is the record store and its transaction: one lock, no
269
+ timing, no process, no signal. A field a record gains and a write a verb
270
+ hands over go here.
271
+ - `pp/lib/jobcore.py` is the worker and the claims: starting, stopping,
272
+ reaping and adopting a job's processes, the worker's loop, and the watchers.
273
+ A bug here loses a job, which is why it is the module held still.
274
+ - `pp/lib/jobpolicy.py` is every rule that is values in and one value out: how
275
+ a job ends, what a selection names, what a setting comes to, what a job's
276
+ environment is. A rule goes here, and a table tests it.
277
+ - `pp/lib/jobq.py` is the verbs, the views, the readings, the one settings
278
+ declaration and the CLI, and `pp/commands/queue.py` is the command and its
279
+ help. Every sentence a user reads is worded here.
280
+
281
+ The store and the worker together are the core. These rules hold that line,
282
+ and `tests/test_constructs.py` reads the source for each:
283
+
284
+ - The boundary rule. Outside the core no function takes the store lock, and
285
+ none starts a job, a runner, a stop command, a hook or a worker, or signals
286
+ a job, a worker or a process group: a verb goes through the core's API, where
287
+ every write is one call and one whole transaction. Two sites outside it run a
288
+ process of their own, and both are named exemptions: `reading`, which runs a
289
+ user's probe under that probe's own claim, and `_cards`, which runs
290
+ nvidia-smi where `[resources] gpu_gb` gives no probe of its own.
291
+ - The purity rule. A function of `jobpolicy.py` reaches no clock, no store, no
292
+ process, no signal and no file. It is handed records, readings, settings and
293
+ `now`, and gives back a value, so a rule that could be raced is a rule no
294
+ table tests.
295
+ - The import rule. `jobpolicy.py` imports nothing of pp's, the store imports
296
+ nothing of the worker, and neither imports the verbs above them. The
297
+ direction is a line, so no cycle can hide a rule that reads a mechanism.
298
+ - The core reads no config file and takes no reading of its own: `read_settings`
299
+ hands a worker the settings of one pass of its loop, and `admit_for` the
300
+ readings of one launch pass. It writes no sentence either: it tells a sink
301
+ what it did, and `jobq` words every one of them.
302
+ - The budgets, in code lines that are neither blank, comment nor docstring:
303
+ 250 for the store, 490 for the worker, 730 for the two together. A feature
304
+ that cannot be built without going over one of them waits until something
305
+ leaves the core, and moving a rule out into `jobpolicy.py` is the first thing
306
+ to try.
307
+ - Every function of the store and of the worker is named in one of the two
308
+ lists in that file, so a construct is placed when it lands, and a name that
309
+ has gone comes off the list in the same change.
310
+
249
311
  ### Where a queue's jobs live, and how one says it is alive
250
312
 
251
- The docstring of `pp/lib/jobq.py` is the reference: the files a queue is made
252
- of, how a worker and a job say they are alive, and the one actor that ends a
253
- job. Each mechanism inside it has one owner, and these are they:
313
+ The docstring of `pp/lib/jobstore.py` is the reference for the files a queue
314
+ is made of, and `pp/lib/jobcore.py`'s for how a worker and a job say they are
315
+ alive and which actor ends a job. Each mechanism has one owner, and these are
316
+ they:
254
317
 
255
- - the store: `Txn`, `txn`, `listing` and `find`. `open_queue` is where a
256
- queue's `FORMAT` is checked, and `Txn.add` is where an id and the record
257
- that holds it land together.
318
+ - the store: `Txn`, `txn`, `listing` and `find`, in `pp/lib/jobstore.py`.
319
+ `open_queue` is where a queue's `FORMAT` is checked, and `Txn.add` is where
320
+ an id and the record that holds it land together. The writes a verb hands
321
+ over are the four at the end of that file, each one a whole transaction of
322
+ its own that is told the states it may act on.
258
323
  - being alive: `lock.Claim`. A claim is held for as long as its holder lives,
259
324
  so nothing is cleaned up after a holder that was killed and no record is
260
325
  ever read as a live holder. A queue's worker holds one on `worker.lock`,
@@ -262,9 +327,13 @@ job. Each mechanism inside it has one owner, and these are they:
262
327
  - whether a queue's worker is up: the block comment above `worker_up`, for
263
328
  the two things that can be true of that claim and the first moment that is
264
329
  neither.
265
- - how a job ends: `decide` and `merge_stop`, which are pure, and
266
- `Worker.carry_out`, the one pass that gathers the facts and carries the
267
- action out.
330
+ - how a job ends: `decide` and `merge_stop` in `pp/lib/jobpolicy.py`, which
331
+ are pure, as every rule in that module is, and `Worker.carry_out`, the one
332
+ pass that gathers the facts and carries the action out.
333
+ - what a worker is handed: it reads no config file and takes no reading of
334
+ its own, so `read_settings` gives it the settings of one pass of its loop
335
+ and `admit_for` the readings of one launch pass. It writes no sentence
336
+ either: it tells a sink what it did, and `jobq` words every one of them.
268
337
  - what a stop carries: `public()`, where every field of a job record that
269
338
  others rely on is named.
270
339
 
@@ -281,9 +350,10 @@ the same place.
281
350
  A kill, pass by pass:
282
351
 
283
352
  ```
284
- kill: txn(merge_stop) -> worker_up(wake=True) -> SIGUSR1 to the worker
285
- worker: carry_out: the facts -> decide = TERM -> signal the group,
286
- then txn marks `sent` -> (the grace) -> decide = KILL9 -> signal
353
+ kill: ask_stop(merge_stop) -> worker_up(wake=True) -> SIGUSR1 to the worker
354
+ worker: carry_out: the facts -> decide = TERM -> the project's stop command,
355
+ or the SIGTERM to the group, then txn marks `sent`
356
+ -> (the grace) -> decide = KILL9 -> signal
287
357
  -> the wrapper ends, so the job's run claim is free
288
358
  -> decide = ENDED_AS -> _finish records the outcome and the hook it owes
289
359
  -> fire
@@ -297,9 +367,11 @@ own exit time and not the queue's poll period.
297
367
 
298
368
  The rules the worker follows are the ones `pp help queue` states under "How a
299
369
  job ends, whatever reached it", and that help text is their one statement.
300
- One more is for contributors: which of the two a stop is comes from the
370
+ Two more are for contributors: which of the two a stop is comes from the
301
371
  `timeout` field a worker writes on the stop it wrote itself, never from the
302
- name of whoever asked.
372
+ name of whoever asked; and an exit code the attempt wrote after its stop went
373
+ out is that stop's outcome, with the code kept, since a container runner writes
374
+ the code of a container the stop command ended.
303
375
 
304
376
  `TestTheDecision` in `tests/test_queue.py` is a table over every combination
305
377
  of those facts. Each row carries the rule from that help as its own label,