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.
- ppkit-0.2.1/CHANGELOG.md +346 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/CONTRIBUTING.md +98 -26
- {ppkit-0.1.0 → ppkit-0.2.1}/PKG-INFO +65 -22
- {ppkit-0.1.0 → ppkit-0.2.1}/README.md +63 -20
- {ppkit-0.1.0 → ppkit-0.2.1}/bin/pp +4 -2
- {ppkit-0.1.0 → ppkit-0.2.1}/ci/check +2 -2
- {ppkit-0.1.0 → ppkit-0.2.1}/pp/__init__.py +1 -1
- {ppkit-0.1.0 → ppkit-0.2.1}/pp/cli.py +158 -22
- {ppkit-0.1.0 → ppkit-0.2.1}/pp/commands/batch.py +299 -31
- {ppkit-0.1.0 → ppkit-0.2.1}/pp/commands/cmd.py +288 -45
- ppkit-0.2.1/pp/commands/completion.py +66 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/pp/commands/http.py +36 -12
- {ppkit-0.1.0 → ppkit-0.2.1}/pp/commands/kill.py +23 -3
- {ppkit-0.1.0 → ppkit-0.2.1}/pp/commands/promote.py +402 -77
- ppkit-0.2.1/pp/commands/queue.py +449 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/pp/lib/config.py +10 -15
- ppkit-0.2.1/pp/lib/jobcore.py +913 -0
- ppkit-0.2.1/pp/lib/jobpolicy.py +1222 -0
- ppkit-0.2.1/pp/lib/jobq.py +2211 -0
- ppkit-0.2.1/pp/lib/jobstore.py +486 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/pp/lib/paths.py +14 -8
- {ppkit-0.1.0 → ppkit-0.2.1}/pp/lib/proc.py +1 -1
- ppkit-0.2.1/pp/lib/session.py +97 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/pp/lib/ui.py +101 -8
- {ppkit-0.1.0 → ppkit-0.2.1}/pyproject.toml +2 -2
- {ppkit-0.1.0 → ppkit-0.2.1}/tests/journey.py +21 -1
- {ppkit-0.1.0 → ppkit-0.2.1}/tests/sandbox.py +5 -1
- {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_batch.py +200 -0
- ppkit-0.2.1/tests/test_cli.py +283 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_cmd.py +55 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_constructs.py +463 -172
- {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_http.py +98 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_journey_batch.py +25 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_journey_cli.py +175 -9
- {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_journey_cmd.py +185 -5
- {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_journey_http.py +2 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_journey_kill.py +4 -1
- {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_journey_promote.py +18 -0
- ppkit-0.2.1/tests/test_journey_queue.py +1530 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_kill.py +31 -0
- ppkit-0.2.1/tests/test_promote.py +1336 -0
- ppkit-0.2.1/tests/test_queue.py +6271 -0
- ppkit-0.2.1/tests/test_session.py +65 -0
- ppkit-0.2.1/tests/test_ui.py +319 -0
- ppkit-0.1.0/CHANGELOG.md +0 -20
- ppkit-0.1.0/pp/commands/completion.py +0 -55
- ppkit-0.1.0/pp/commands/queue.py +0 -243
- ppkit-0.1.0/pp/lib/jobq.py +0 -3106
- ppkit-0.1.0/pp/lib/session.py +0 -31
- ppkit-0.1.0/tests/test_cli.py +0 -166
- ppkit-0.1.0/tests/test_journey_queue.py +0 -637
- ppkit-0.1.0/tests/test_promote.py +0 -679
- ppkit-0.1.0/tests/test_queue.py +0 -3280
- ppkit-0.1.0/tests/test_session.py +0 -24
- ppkit-0.1.0/tests/test_ui.py +0 -107
- {ppkit-0.1.0 → ppkit-0.2.1}/.pp.toml +0 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/LICENSE +0 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/ci/package +0 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/pp/__main__.py +0 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/pp/commands/__init__.py +0 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/pp/lib/__init__.py +0 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/pp/lib/lock.py +0 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_lock.py +0 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_paths.py +0 -0
- {ppkit-0.1.0 → ppkit-0.2.1}/tests/test_proc.py +0 -0
ppkit-0.2.1/CHANGELOG.md
ADDED
|
@@ -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
|
|
38
|
-
ci/check --cpus 2 3.11
|
|
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
|
|
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/
|
|
189
|
-
|
|
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/
|
|
218
|
-
| `worker-claim` | taking a queue's worker claim, or reading its holder | `worker`, `worker_now`, `work` | `pp/lib/
|
|
219
|
-
| `job-process` | signalling or reaping a job's processes | the `Worker` class, `
|
|
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/
|
|
252
|
-
of, how a worker and a job say they are
|
|
253
|
-
job. Each mechanism
|
|
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
|
|
256
|
-
queue's `FORMAT` is checked, and `Txn.add` is where
|
|
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
|
|
266
|
-
|
|
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:
|
|
285
|
-
worker: carry_out: the facts -> decide = TERM ->
|
|
286
|
-
|
|
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
|
-
|
|
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,
|