processkit-py 1.1.1__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {processkit_py-1.1.1 → processkit_py-1.2.0}/CHANGELOG.md +128 -1
- {processkit_py-1.1.1 → processkit_py-1.2.0}/CONTRIBUTING.md +5 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/Cargo.lock +3 -3
- {processkit_py-1.1.1 → processkit_py-1.2.0}/Cargo.toml +1 -1
- {processkit_py-1.1.1 → processkit_py-1.2.0}/PKG-INFO +13 -2
- {processkit_py-1.1.1 → processkit_py-1.2.0}/README.md +12 -1
- {processkit_py-1.1.1 → processkit_py-1.2.0}/RELEASING.md +6 -10
- {processkit_py-1.1.1 → processkit_py-1.2.0}/SECURITY.md +8 -0
- processkit_py-1.2.0/benchmarks/README.md +57 -0
- processkit_py-1.2.0/benchmarks/__init__.py +11 -0
- processkit_py-1.2.0/benchmarks/_shared.py +20 -0
- processkit_py-1.2.0/benchmarks/test_output_all.py +52 -0
- processkit_py-1.2.0/benchmarks/test_process_group.py +26 -0
- processkit_py-1.2.0/benchmarks/test_spawn_capture.py +61 -0
- processkit_py-1.2.0/benchmarks/test_streaming_throughput.py +41 -0
- processkit_py-1.2.0/deny.toml +44 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/docs/README.md +5 -2
- {processkit_py-1.1.1 → processkit_py-1.2.0}/docs/api-reference.md +3 -1
- processkit_py-1.2.0/docs/cli.md +114 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/docs/commands.md +22 -4
- {processkit_py-1.1.1 → processkit_py-1.2.0}/docs/cookbook.md +75 -14
- {processkit_py-1.1.1 → processkit_py-1.2.0}/docs/event-loops.md +4 -4
- {processkit_py-1.1.1 → processkit_py-1.2.0}/docs/internals.md +62 -18
- {processkit_py-1.1.1 → processkit_py-1.2.0}/docs/migrating.md +1 -1
- {processkit_py-1.1.1 → processkit_py-1.2.0}/docs/process-groups.md +15 -0
- processkit_py-1.2.0/docs/sandboxing.md +208 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/docs/streaming.md +109 -31
- {processkit_py-1.1.1 → processkit_py-1.2.0}/docs/supervision.md +40 -2
- {processkit_py-1.1.1 → processkit_py-1.2.0}/docs/testing.md +1 -1
- {processkit_py-1.1.1 → processkit_py-1.2.0}/docs/timeouts-and-cancellation.md +7 -5
- processkit_py-1.2.0/examples/04_sandbox_resource_limits.py +98 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/examples/README.md +4 -2
- {processkit_py-1.1.1 → processkit_py-1.2.0}/mkdocs.yml +2 -1
- {processkit_py-1.1.1 → processkit_py-1.2.0}/pyproject.toml +52 -3
- {processkit_py-1.1.1 → processkit_py-1.2.0}/scripts/ci-privileged-guard.py +7 -3
- {processkit_py-1.1.1 → processkit_py-1.2.0}/scripts/gen_api_reference.py +3 -2
- {processkit_py-1.1.1 → processkit_py-1.2.0}/scripts/release/cargo_lock.py +4 -1
- {processkit_py-1.1.1 → processkit_py-1.2.0}/scripts/release/changelog.py +31 -12
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/batch.rs +10 -7
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/cli.rs +36 -2
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/command.rs +186 -47
- processkit_py-1.2.0/src/convert.rs +848 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/group.rs +56 -24
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/lib.rs +8 -2
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/processkit/__init__.py +2 -1
- processkit_py-1.2.0/src/processkit/__main__.py +263 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/processkit/_aio.py +101 -11
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/processkit/_processkit.pyi +206 -82
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/processkit/_protocols.py +5 -5
- processkit_py-1.2.0/src/processkit/_types.py +117 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/processkit/pytest_plugin.py +7 -0
- processkit_py-1.2.0/src/result.rs +867 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/runner.rs +155 -63
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/running.rs +86 -53
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/runtime.rs +1 -8
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/supervisor.rs +209 -8
- processkit_py-1.2.0/tests/_liveness.py +195 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/_programs.py +16 -6
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/_typing_pins.py +81 -3
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/property/test_argv_env_roundtrip.py +2 -2
- processkit_py-1.2.0/tests/test_batch.py +362 -0
- processkit_py-1.2.0/tests/test_ci_privileged_guard.py +102 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_cli_client.py +16 -2
- processkit_py-1.2.0/tests/test_cli_main.py +138 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_command.py +624 -4
- processkit_py-1.2.0/tests/test_event_loops.py +235 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_hardening.py +8 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_logging.py +20 -1
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_pipelines.py +37 -8
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_pytest_plugin.py +22 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_readiness.py +151 -4
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_release_scripts.py +129 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_streaming.py +140 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_supervisor.py +30 -0
- processkit_py-1.2.0/tests/test_threading.py +521 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/uv.lock +314 -217
- processkit_py-1.1.1/examples/04_sandbox_resource_limits.py +0 -82
- processkit_py-1.1.1/src/convert.rs +0 -289
- processkit_py-1.1.1/src/processkit/_types.py +0 -52
- processkit_py-1.1.1/src/result.rs +0 -458
- processkit_py-1.1.1/tests/_liveness.py +0 -97
- processkit_py-1.1.1/tests/test_batch.py +0 -164
- {processkit_py-1.1.1 → processkit_py-1.2.0}/.editorconfig +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/.gitattributes +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/.gitignore +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/.pre-commit-config.yaml +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/.python-version +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/.yamllint.yml +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/LICENSE +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/ROADMAP.md +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/cliff.toml +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/conftest.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/docs/pipelines.md +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/docs/platforms.md +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/examples/01_no_orphan_guarantee.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/examples/02_wait_for_server.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/examples/03_supervise_until_healthy.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/rust-toolchain.toml +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/scripts/check-env.ps1 +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/scripts/check-env.sh +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/scripts/ci-privileged-check.sh +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/scripts/release/__init__.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/scripts/smoke.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/cancellation.rs +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/errors.rs +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/logging.rs +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/processkit/py.typed +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/src/processkit/testing.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/stubtest-allowlist.txt +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/__init__.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/_docs_snippets.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/conftest.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/property/__init__.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/property/conftest.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/property/test_numeric_validation.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/property/test_output_limit.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/property/test_signals.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/property/test_wait_for_line.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_api_reference.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_api_surface.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_async.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_docs_snippets.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_examples.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_exceptions.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_process_group.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.0}/tests/test_runner_seam.py +0 -0
|
@@ -16,6 +16,132 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
16
|
### Fixed
|
|
17
17
|
-
|
|
18
18
|
|
|
19
|
+
## [1.2.0] - 2026-07-08
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- `Command.stdout_tee` / `Command.stderr_tee` now accept a **Python writer**
|
|
23
|
+
object (anything with a callable `write()` — `io.StringIO`, `sys.stderr`, a
|
|
24
|
+
text-mode file, a logger wrapper) in addition to a file path, mirroring the
|
|
25
|
+
child's output straight into your own console/buffer/logger while still
|
|
26
|
+
capturing it. Each decoded line (plus a `"\n"`) is passed to `write()` as a
|
|
27
|
+
`str` via an async-write bridge: every write is dispatched to the runtime's
|
|
28
|
+
blocking pool (re-acquiring the GIL there) and awaited on the capture pump, so
|
|
29
|
+
a slow — even sleeping — `write()` applies backpressure without blocking the
|
|
30
|
+
event loop or deadlocking the runtime. The object is discriminated from a path
|
|
31
|
+
by exposing `write` (neither `str` nor `pathlib.Path` does) and is never closed
|
|
32
|
+
for you; `append=True` is meaningful only for a file path and raises
|
|
33
|
+
`ValueError` if combined with a writer. A `write()` exception disables the tee
|
|
34
|
+
for the rest of the run (a `tracing` warning under `enable_logging()`, the same
|
|
35
|
+
isolation as the file tee) and is additionally reported via `sys.unraisablehook`
|
|
36
|
+
— the run and its captured result are unaffected. The previous "a file path
|
|
37
|
+
only, an arbitrary Python writer is deliberately not supported" restriction is
|
|
38
|
+
lifted. See `docs/streaming.md#tee-output-to-a-file`.
|
|
39
|
+
- `Command.on_stdout_line(callback)` / `Command.on_stderr_line(callback)`: a
|
|
40
|
+
`Callable[[str], None]` invoked with every decoded line as it is produced —
|
|
41
|
+
the way to give the **synchronous** surface (`.output()`/`.run()`) live
|
|
42
|
+
progress observation during an otherwise-blocking call, without losing the
|
|
43
|
+
full capture. Also fires on the async verbs and on a streamed run
|
|
44
|
+
(`start()`/`astart()` + `stdout_lines()`/`output_events()`); at most one
|
|
45
|
+
handler per stream (a repeat call replaces the previous one); a raising
|
|
46
|
+
callback is reported via `sys.unraisablehook` rather than propagated or
|
|
47
|
+
breaking the run. Inert under `stdout("inherit")`/`stdout("null")` (resp.
|
|
48
|
+
`stderr(...)`) and, for `on_stdout_line` only, under `output_bytes()` (which
|
|
49
|
+
captures stdout raw, bypassing the line pump — stderr still goes through it,
|
|
50
|
+
so `on_stderr_line` still fires there). See
|
|
51
|
+
`docs/streaming.md#live-per-line-callbacks`.
|
|
52
|
+
- A `benchmarks/` suite (`pytest-benchmark`, new `bench` dependency-group)
|
|
53
|
+
measuring spawn+capture overhead against `subprocess`/`asyncio.subprocess`,
|
|
54
|
+
`ProcessGroup` start/exit, line-streaming throughput, and `output_all`
|
|
55
|
+
concurrency scaling — dev tooling only, no public API change. Runs nightly
|
|
56
|
+
via the `bench` job in `nightly-hardening.yml`, never in the PR gate; see
|
|
57
|
+
`benchmarks/README.md`.
|
|
58
|
+
- `wait_for_path(path, *, timeout, interval=0.05)` — a new async readiness
|
|
59
|
+
helper alongside `wait_until` / `wait_for_port` / `wait_for_line`, polling
|
|
60
|
+
until a filesystem path appears (a unix socket, a pid file, or any other
|
|
61
|
+
marker a daemon creates once ready). Same timeout/interval discipline as its
|
|
62
|
+
siblings (NaN/negative `timeout` and non-positive `interval` raise
|
|
63
|
+
`ValueError`; `timeout=0` still checks the path at least once) and raises
|
|
64
|
+
`WaitTimeout` (also a `TimeoutError`) on expiry, now carrying a `path` field
|
|
65
|
+
(`WaitTimeout.__init__` gained a `path: StrPath | None = None` parameter).
|
|
66
|
+
- `python -m processkit run -- <cmd> [args...]`: a CLI wrapper that runs a
|
|
67
|
+
command inside a kill-on-exit `ProcessGroup` with inherited stdio, for
|
|
68
|
+
shell scripts and CI steps with no Python to write. Supports `--timeout`,
|
|
69
|
+
`--timeout-grace`, `--max-memory`, `--max-processes`, and `--cpu-quota`;
|
|
70
|
+
the child's own exit code is passed through unchanged,
|
|
71
|
+
and a timeout / missing program / rejected resource limit is reported as a
|
|
72
|
+
one-line stderr message with a documented, GNU-`timeout`-style exit code
|
|
73
|
+
instead of a traceback. See `docs/cli.md`.
|
|
74
|
+
- `Finished` gains `timed_out` and `signal` properties that delegate to the
|
|
75
|
+
nested `outcome`, so it now mirrors `Outcome` fully — matching `code` and
|
|
76
|
+
`exited_zero`, which were already exposed directly — instead of requiring
|
|
77
|
+
`finished.outcome.timed_out` / `finished.outcome.signal`.
|
|
78
|
+
- `Command.stdin_file(path)` — feed the child's stdin from a file, streamed in
|
|
79
|
+
chunks by the crate rather than read whole into a Python `bytes` object, for
|
|
80
|
+
large inputs (a `psql` dump, a `tar` archive, a multi-gigabyte log). Like
|
|
81
|
+
most other builder methods (`stdout_tee`/`stderr_tee` are the deliberate
|
|
82
|
+
exception), it does not touch the filesystem at build time — the path is
|
|
83
|
+
opened lazily at spawn, so a missing/unreadable file surfaces as the generic
|
|
84
|
+
`ProcessError` from the run/output verb, not `FileNotFoundError`. Reusable
|
|
85
|
+
across retries/re-runs, like `stdin_bytes`/`stdin_text`; the usual "last
|
|
86
|
+
stdin method wins" rule applies alongside `stdin_bytes()`/`stdin_text()`/
|
|
87
|
+
`keep_stdin_open()`.
|
|
88
|
+
- `ProcessResult` and `BytesResult` gain `diagnostic: str | None` (stderr if it
|
|
89
|
+
carries text, otherwise stdout, otherwise `None` — the same preference order
|
|
90
|
+
as `NonZeroExit`/`Timeout`/`Signalled.diagnostic` on the exceptions) and
|
|
91
|
+
`outcome: Outcome` (the same value `RunProfile.outcome` and the checking-verb
|
|
92
|
+
exceptions expose). A result held as data (`output()`/`output_bytes()`
|
|
93
|
+
without `ensure_success()`) no longer requires re-deriving these by hand.
|
|
94
|
+
(An `output_contains_any` convenience was considered alongside these and
|
|
95
|
+
rejected: the underlying `processkit` crate has no such method, so it
|
|
96
|
+
wouldn't be parity with the crate or the exceptions like `diagnostic`/
|
|
97
|
+
`outcome` are — and it's a one-liner callers can already write themselves
|
|
98
|
+
via `combined`, e.g. `any(s in result.combined for s in needles)`.)
|
|
99
|
+
- Value semantics for the result types: `ProcessResult`, `BytesResult`,
|
|
100
|
+
`Outcome`, `Finished`, `RunProfile`, and `SupervisionOutcome` now define
|
|
101
|
+
`__eq__` (comparing every field the underlying `processkit` crate's own
|
|
102
|
+
`PartialEq` compares — not `object`'s previous identity comparison) and a
|
|
103
|
+
consistent `__hash__` (none of their fields are stored floats, so hashing is
|
|
104
|
+
sound), so two results can now be compared with `==` and used in a `set` or
|
|
105
|
+
as a `dict` key without a manual field-by-field comparison.
|
|
106
|
+
`ProcessResult`/`Outcome`/`Finished`/`SupervisionOutcome` are also picklable
|
|
107
|
+
— e.g. to return a `ProcessResult` from a
|
|
108
|
+
`concurrent.futures.ProcessPoolExecutor` worker. The underlying crate has no
|
|
109
|
+
public constructor for any of these types, so unpickling reconstructs one via
|
|
110
|
+
`processkit.testing.ScriptedRunner` (an in-memory, no-subprocess replay) —
|
|
111
|
+
faithful for every field the Python binding exposes, but a command that
|
|
112
|
+
customized `success_codes()`/`timeout()` is not guaranteed to compare `==`
|
|
113
|
+
its original after a round trip (those two fields have no Python accessor to
|
|
114
|
+
reconstruct exactly). `BytesResult` (raw stdout may not be valid UTF-8, and
|
|
115
|
+
the only reconstruction channel available is text-only) and `RunProfile`
|
|
116
|
+
(reports live OS resource-sampling telemetry with no synthesis path outside
|
|
117
|
+
an actual monitored run) explicitly do **not** support pickling and raise a
|
|
118
|
+
clear `TypeError` rather than failing silently or fabricating the missing
|
|
119
|
+
data.
|
|
120
|
+
|
|
121
|
+
### Changed
|
|
122
|
+
- `CliClient(default_env_fn=...)` now validates that every value in the
|
|
123
|
+
mapping is callable **at construction time**, raising `TypeError` (naming
|
|
124
|
+
the offending key) immediately instead of silently accepting a non-callable
|
|
125
|
+
value and only discovering the mistake later — once per built command, as
|
|
126
|
+
an unraisable-hook warning plus an always-empty resolved env var. Valid
|
|
127
|
+
callables behave exactly as before.
|
|
128
|
+
|
|
129
|
+
### Fixed
|
|
130
|
+
- `Args` (`from processkit import Args`) no longer rejects the single most
|
|
131
|
+
common real call site — a variable annotated `list[str]` (or
|
|
132
|
+
`list[pathlib.Path]` / `list[os.PathLike[str]]`) passed straight through to
|
|
133
|
+
an argv-like parameter, e.g. `args: list[str] = [...]; cmd.args(args)`.
|
|
134
|
+
`list` is invariant, so the original `list[StrPath] | tuple[StrPath, ...]`
|
|
135
|
+
spelling only ever accepted a `list[StrPath]`-annotated variable or a
|
|
136
|
+
literal, not a `list[str]`/`list[Path]`/`list[os.PathLike[str]]`-annotated
|
|
137
|
+
one, even though the values are runtime-identical — a static-typing-only
|
|
138
|
+
false positive with no runtime effect. `Args` is now a union of the concrete
|
|
139
|
+
homogeneous list shapes (`list[str]`, `list[Path]`, `list[os.PathLike[str]]`)
|
|
140
|
+
instead of the single invariant `list[StrPath]`; a *mixed* `str`/
|
|
141
|
+
`os.PathLike[str]` argv is still accepted, now spelled as a `tuple` rather
|
|
142
|
+
than a `list` literal (e.g. `cmd.args((path, "literal"))`). A bare `str`
|
|
143
|
+
still does not type-check as `Args` (unchanged; see the `Args` docstring).
|
|
144
|
+
|
|
19
145
|
## [1.1.1] - 2026-07-06
|
|
20
146
|
|
|
21
147
|
### Added
|
|
@@ -709,7 +835,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
709
835
|
- `enable_logging()` enables the crate's `tracing` feature; the bridge pulls
|
|
710
836
|
`tracing` / `tracing-subscriber` (registry only) into the compiled wheel.
|
|
711
837
|
|
|
712
|
-
[Unreleased]: https://github.com/ZelAnton/processkit-py/compare/v1.
|
|
838
|
+
[Unreleased]: https://github.com/ZelAnton/processkit-py/compare/v1.2.0...HEAD
|
|
839
|
+
[1.2.0]: https://github.com/ZelAnton/processkit-py/compare/v1.1.1...v1.2.0
|
|
713
840
|
[1.1.1]: https://github.com/ZelAnton/processkit-py/compare/v1.1.0...v1.1.1
|
|
714
841
|
[1.1.0]: https://github.com/ZelAnton/processkit-py/compare/v1.0.0...v1.1.0
|
|
715
842
|
[1.0.0]: https://github.com/ZelAnton/processkit-py/releases/tag/v1.0.0
|
|
@@ -80,6 +80,11 @@ native `uv run pytest`, which is faster for day-to-day work.
|
|
|
80
80
|
warning-free `pytest`, plus `cargo fmt` / `clippy` on the Rust side — all
|
|
81
81
|
configured in [`pyproject.toml`](pyproject.toml); run the
|
|
82
82
|
[gates above](#build-and-test) locally before opening a pull request.
|
|
83
|
+
- **Docs are built here, not published here.** `docs.yml` runs `mkdocs build
|
|
84
|
+
--strict` as a link/anchor check only; this repo does not deploy to GitHub
|
|
85
|
+
Pages or anywhere else (a separate project owns docs publishing). Do not
|
|
86
|
+
add a Pages/`mike`/`gh-pages` deploy step back — see RELEASING.md's "Docs
|
|
87
|
+
site" note.
|
|
83
88
|
|
|
84
89
|
## Changelog
|
|
85
90
|
|
|
@@ -165,9 +165,9 @@ dependencies = [
|
|
|
165
165
|
|
|
166
166
|
[[package]]
|
|
167
167
|
name = "processkit"
|
|
168
|
-
version = "2.1.
|
|
168
|
+
version = "2.1.1"
|
|
169
169
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
170
|
-
checksum = "
|
|
170
|
+
checksum = "ccf43ce2a1756fe36382bc56b0bf1697d9ee1c32c0ed5a2afb5abf5163a64756"
|
|
171
171
|
dependencies = [
|
|
172
172
|
"async-trait",
|
|
173
173
|
"encoding_rs",
|
|
@@ -184,7 +184,7 @@ dependencies = [
|
|
|
184
184
|
|
|
185
185
|
[[package]]
|
|
186
186
|
name = "processkit-py"
|
|
187
|
-
version = "1.
|
|
187
|
+
version = "1.2.0"
|
|
188
188
|
dependencies = [
|
|
189
189
|
"processkit",
|
|
190
190
|
"pyo3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: processkit-py
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -102,7 +102,7 @@ The first column is the differentiator: a child's *descendants* are contained an
|
|
|
102
102
|
reaped as a unit (Job Object / cgroup v2 / process group), not just the direct
|
|
103
103
|
child.
|
|
104
104
|
|
|
105
|
-
> **
|
|
105
|
+
> **Stable API.** The public API has been stable since 1.0 and follows
|
|
106
106
|
> [Semantic Versioning](https://semver.org/): breaking changes land only in a new
|
|
107
107
|
> major version, so `1.x` upgrades are backward-compatible. See
|
|
108
108
|
> [CHANGELOG.md](CHANGELOG.md), and [ROADMAP.md](ROADMAP.md) for how it was built.
|
|
@@ -186,6 +186,17 @@ async def main():
|
|
|
186
186
|
asyncio.run(main())
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
+
No Python to write? `python -m processkit run -- <cmd> [args...]` gives a
|
|
190
|
+
shell script or CI step the same kill-on-exit containment and resource
|
|
191
|
+
limits from the command line, no code required:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
python -m processkit run --timeout 30 --max-memory 536870912 -- pytest -x
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
See [Command-line usage](docs/cli.md) for the full flag list and exit-code
|
|
198
|
+
contract.
|
|
199
|
+
|
|
189
200
|
## Documentation
|
|
190
201
|
|
|
191
202
|
This README is the quick tour. The **[`docs/` guide set](docs/README.md)** goes
|
|
@@ -69,7 +69,7 @@ The first column is the differentiator: a child's *descendants* are contained an
|
|
|
69
69
|
reaped as a unit (Job Object / cgroup v2 / process group), not just the direct
|
|
70
70
|
child.
|
|
71
71
|
|
|
72
|
-
> **
|
|
72
|
+
> **Stable API.** The public API has been stable since 1.0 and follows
|
|
73
73
|
> [Semantic Versioning](https://semver.org/): breaking changes land only in a new
|
|
74
74
|
> major version, so `1.x` upgrades are backward-compatible. See
|
|
75
75
|
> [CHANGELOG.md](CHANGELOG.md), and [ROADMAP.md](ROADMAP.md) for how it was built.
|
|
@@ -153,6 +153,17 @@ async def main():
|
|
|
153
153
|
asyncio.run(main())
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
+
No Python to write? `python -m processkit run -- <cmd> [args...]` gives a
|
|
157
|
+
shell script or CI step the same kill-on-exit containment and resource
|
|
158
|
+
limits from the command line, no code required:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
python -m processkit run --timeout 30 --max-memory 536870912 -- pytest -x
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
See [Command-line usage](docs/cli.md) for the full flag list and exit-code
|
|
165
|
+
contract.
|
|
166
|
+
|
|
156
167
|
## Documentation
|
|
157
168
|
|
|
158
169
|
This README is the quick tour. The **[`docs/` guide set](docs/README.md)** goes
|
|
@@ -68,17 +68,13 @@ GitHub Release (wheels + sdist + `SHA256SUMS`).
|
|
|
68
68
|
## Docs site
|
|
69
69
|
|
|
70
70
|
The guides in `docs/` render as a [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/)
|
|
71
|
-
site
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
site. `docs.yml` builds it (`mkdocs build --strict`) on every docs change as a
|
|
72
|
+
link/anchor check only — this repo does **not** publish the site (no GitHub
|
|
73
|
+
Pages, no `gh-pages` branch, no `mike`, no `DOCS_DEPLOY`). A separate project
|
|
74
|
+
owns docs publishing. **Do not re-add a Pages/mike deploy job here** — if a
|
|
75
|
+
future dependency bump or template sync tries to reintroduce one, undo it.
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
2. Repo **Settings → Secrets and variables → Actions → Variables**: add
|
|
77
|
-
`DOCS_DEPLOY` = `true`.
|
|
78
|
-
|
|
79
|
-
The next push to `main` that touches `docs/` or `mkdocs.yml` then deploys to
|
|
80
|
-
`https://zelanton.github.io/processkit-py/`. Preview locally with
|
|
81
|
-
`uvx --with mkdocs-material mkdocs serve`.
|
|
77
|
+
Preview the docs locally with `uv run --group docs mkdocs serve`.
|
|
82
78
|
|
|
83
79
|
## If a release fails
|
|
84
80
|
|
|
@@ -36,6 +36,14 @@ ready, a patched release is published to PyPI and the advisory is disclosed.
|
|
|
36
36
|
[`.github/workflows/ci.yml`](.github/workflows/ci.yml)). It scans the resolved
|
|
37
37
|
Python dependency tree against the [PyPI Advisory Database](https://github.com/pypa/advisory-database)
|
|
38
38
|
and fails the build on a known vulnerability.
|
|
39
|
+
- **[cargo-deny](https://github.com/EmbarkStudios/cargo-deny)** is the Rust
|
|
40
|
+
analogue of pip-audit: the `rust-audit` job in
|
|
41
|
+
[`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs
|
|
42
|
+
`cargo deny check advisories bans licenses sources` on every pull request
|
|
43
|
+
and every push to `main`, scanning the compiled Rust dependency tree
|
|
44
|
+
(configured in [`deny.toml`](deny.toml)) against the RustSec Advisory
|
|
45
|
+
Database and failing the build on a known vulnerability, a yanked crate, or
|
|
46
|
+
a disallowed license.
|
|
39
47
|
- **[Dependabot](.github/dependabot.yml)** opens weekly pull requests to keep
|
|
40
48
|
GitHub Actions, Python packages (`uv` ecosystem), and Rust crates (`cargo`
|
|
41
49
|
ecosystem) current, so advisory fixes land promptly.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Benchmarks
|
|
2
|
+
|
|
3
|
+
`pytest-benchmark` timings for the questions [ROADMAP.md](../ROADMAP.md)'s
|
|
4
|
+
Phase 5 asks — "does the bridge add silly overhead?" — with a real number
|
|
5
|
+
attached instead of only the loose pass/fail bound in
|
|
6
|
+
`tests/test_hardening.py::test_no_silly_per_call_overhead`:
|
|
7
|
+
|
|
8
|
+
- **`test_spawn_capture.py`** — spawn + capture a single short-lived command:
|
|
9
|
+
`processkit`'s `Command(...).output()` against the two stdlib "naive"
|
|
10
|
+
equivalents, `subprocess.run(..., capture_output=True)` and
|
|
11
|
+
`asyncio.create_subprocess_exec(...)` + `communicate()`. Same payload on
|
|
12
|
+
all three, so the comparison is per-call overhead, not a differing
|
|
13
|
+
workload.
|
|
14
|
+
- **`test_process_group.py`** — `ProcessGroup` start/exit: creating the
|
|
15
|
+
group's kernel container, entering it, starting one short-lived child,
|
|
16
|
+
tearing the whole tree down.
|
|
17
|
+
- **`test_streaming_throughput.py`** — `RunningProcess.stdout_lines()` (see
|
|
18
|
+
[`docs/streaming.md`](../docs/streaming.md)) draining a known number of
|
|
19
|
+
lines end to end.
|
|
20
|
+
- **`test_output_all.py`** — `output_all()` / `aoutput_all()` at 1/10/50-way
|
|
21
|
+
concurrency (see [`docs/cookbook.md`](../docs/cookbook.md)).
|
|
22
|
+
|
|
23
|
+
## Running locally
|
|
24
|
+
|
|
25
|
+
This suite is **not** part of the PR gate — it lives in its own
|
|
26
|
+
`bench` dependency-group and is excluded from `testpaths` (`tests/` only), so
|
|
27
|
+
an ordinary `pytest`/`uv run pytest` never collects it. Install the group and
|
|
28
|
+
run it explicitly:
|
|
29
|
+
|
|
30
|
+
```console
|
|
31
|
+
uv sync --group bench
|
|
32
|
+
uv run pytest benchmarks/ --benchmark-only -p no:xdist -o addopts=""
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`-p no:xdist -o addopts=""` disables `-n auto` (the repo's default
|
|
36
|
+
`addopts`) — `pytest-benchmark` needs to run in the main process, in a single
|
|
37
|
+
worker, to produce meaningful timings; under `pytest-xdist` it silently skips
|
|
38
|
+
measuring instead.
|
|
39
|
+
|
|
40
|
+
Useful extras:
|
|
41
|
+
|
|
42
|
+
- `--benchmark-only` skips the normal (non-benchmark) test collection outside
|
|
43
|
+
this directory should it ever leak in; harmless here since `benchmarks/`
|
|
44
|
+
has none, but keeps the invocation copy-pasteable elsewhere.
|
|
45
|
+
- `--benchmark-compare` / `--benchmark-autosave` — compare a run against a
|
|
46
|
+
previously saved one, to check a change before landing it.
|
|
47
|
+
- `--benchmark-json=out.json` — machine-readable results (what the nightly
|
|
48
|
+
CI job uses to render the job-summary table; see below).
|
|
49
|
+
|
|
50
|
+
## CI
|
|
51
|
+
|
|
52
|
+
The `bench` job in
|
|
53
|
+
[`.github/workflows/nightly-hardening.yml`](../.github/workflows/nightly-hardening.yml)
|
|
54
|
+
runs this suite on the same `schedule`/`workflow_dispatch` triggers as the
|
|
55
|
+
`stress` job — never on `push`/`pull_request` — and publishes the results as
|
|
56
|
+
a table in the job summary, so a regression shows up as a trend across nights
|
|
57
|
+
rather than only when someone happens to run this locally.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Benchmark suite (`pytest-benchmark`) — overhead vs `subprocess`.
|
|
2
|
+
|
|
3
|
+
Not part of the PR gate: `[tool.pytest.ini_options] testpaths` in
|
|
4
|
+
`pyproject.toml` is `["tests"]`, so an ordinary `pytest` run never collects
|
|
5
|
+
this directory. It runs on a schedule (and on manual dispatch) from the
|
|
6
|
+
`bench` job in `.github/workflows/nightly-hardening.yml`, which installs the
|
|
7
|
+
separate `bench` dependency-group and invokes `pytest benchmarks/
|
|
8
|
+
--benchmark-only` explicitly. See `benchmarks/README.md` to run it locally.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Small pieces shared by the benchmark modules — no test collection here."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
#: The interpreter under test, for building child `Command`s/`subprocess`
|
|
8
|
+
#: calls — same idea as `tests/conftest.py`'s `PY`, kept local so this
|
|
9
|
+
#: directory has no import-time dependency on the `tests/` package.
|
|
10
|
+
PY = sys.executable
|
|
11
|
+
|
|
12
|
+
#: A short-lived child that does no work (the pure per-call-overhead
|
|
13
|
+
#: baseline) — the same style as `tests/test_hardening.py`'s
|
|
14
|
+
#: `test_no_silly_per_call_overhead`.
|
|
15
|
+
NOOP_CODE = "pass"
|
|
16
|
+
|
|
17
|
+
#: A payload that also exercises stdout *capture* (not just spawn/exit),
|
|
18
|
+
#: identical across all three "spawn + capture" benchmarks below so the
|
|
19
|
+
#: comparison isolates per-call overhead rather than differing workloads.
|
|
20
|
+
CAPTURE_CODE = "import sys; sys.stdout.write('x' * 4096)"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""`output_all` / `aoutput_all` at different concurrency levels — bounded
|
|
2
|
+
parallel batches of short-lived commands (see `docs/cookbook.md`'s
|
|
3
|
+
"many commands, bounded concurrency" recipe).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from processkit import Command, aoutput_all, output_all
|
|
14
|
+
|
|
15
|
+
from ._shared import NOOP_CODE, PY
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pytest_benchmark.fixture import BenchmarkFixture
|
|
19
|
+
|
|
20
|
+
#: Concurrency == batch size in every case below: N commands, all allowed to
|
|
21
|
+
#: run at once, at each of the sizes the task calls out (1/10/50) — that
|
|
22
|
+
#: isolates how the batch dispatch machinery itself scales, rather than
|
|
23
|
+
#: mixing in a fixed backlog queued behind a narrower `concurrency=`.
|
|
24
|
+
_CONCURRENCIES = (1, 10, 50)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.parametrize("concurrency", _CONCURRENCIES)
|
|
28
|
+
def test_output_all_concurrency(benchmark: BenchmarkFixture, concurrency: int) -> None:
|
|
29
|
+
commands = [Command(PY, ["-c", NOOP_CODE]) for _ in range(concurrency)]
|
|
30
|
+
benchmark.group = f"output_all(concurrency={concurrency})"
|
|
31
|
+
|
|
32
|
+
def run() -> None:
|
|
33
|
+
results = output_all(commands, concurrency=concurrency)
|
|
34
|
+
assert len(results) == concurrency
|
|
35
|
+
|
|
36
|
+
benchmark(run)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.parametrize("concurrency", _CONCURRENCIES)
|
|
40
|
+
def test_aoutput_all_concurrency(benchmark: BenchmarkFixture, concurrency: int) -> None:
|
|
41
|
+
commands = [Command(PY, ["-c", NOOP_CODE]) for _ in range(concurrency)]
|
|
42
|
+
benchmark.group = f"aoutput_all(concurrency={concurrency})"
|
|
43
|
+
|
|
44
|
+
async def scenario() -> int:
|
|
45
|
+
results = await aoutput_all(commands, concurrency=concurrency)
|
|
46
|
+
return len(results)
|
|
47
|
+
|
|
48
|
+
def run() -> None:
|
|
49
|
+
count = asyncio.run(scenario())
|
|
50
|
+
assert count == concurrency
|
|
51
|
+
|
|
52
|
+
benchmark(run)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""`ProcessGroup` start/exit — the cost of creating the group's kernel
|
|
2
|
+
container (Job Object / cgroup-v2 / process group, see `docs/platforms.md`),
|
|
3
|
+
entering the context manager, starting one short-lived child into it, and
|
|
4
|
+
tearing the whole tree down on exit.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from processkit import Command, ProcessGroup
|
|
12
|
+
|
|
13
|
+
from ._shared import NOOP_CODE, PY
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pytest_benchmark.fixture import BenchmarkFixture
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_process_group_start_exit(benchmark: BenchmarkFixture) -> None:
|
|
20
|
+
def run() -> None:
|
|
21
|
+
with ProcessGroup() as group:
|
|
22
|
+
proc = group.start(Command(PY, ["-c", NOOP_CODE]))
|
|
23
|
+
outcome = proc.outcome()
|
|
24
|
+
assert outcome.exited_zero
|
|
25
|
+
|
|
26
|
+
benchmark(run)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Spawn + capture a single short-lived command: `processkit` vs the two
|
|
2
|
+
"naive" ways of doing the same thing from stdlib — `subprocess.run` and
|
|
3
|
+
`asyncio.create_subprocess_exec` + `communicate()`. Same payload on all
|
|
4
|
+
three (`_shared.CAPTURE_CODE`) so the comparison isolates the bridge's
|
|
5
|
+
per-call overhead rather than a differing workload.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import subprocess
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from processkit import Command
|
|
15
|
+
|
|
16
|
+
from ._shared import CAPTURE_CODE, PY
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pytest_benchmark.fixture import BenchmarkFixture
|
|
20
|
+
|
|
21
|
+
_EXPECTED = "x" * 4096
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_spawn_capture_processkit(benchmark: BenchmarkFixture) -> None:
|
|
25
|
+
def run() -> str:
|
|
26
|
+
result = Command(PY, ["-c", CAPTURE_CODE]).output()
|
|
27
|
+
assert result.is_success
|
|
28
|
+
return result.stdout
|
|
29
|
+
|
|
30
|
+
stdout = benchmark(run)
|
|
31
|
+
assert stdout == _EXPECTED
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_spawn_capture_subprocess(benchmark: BenchmarkFixture) -> None:
|
|
35
|
+
def run() -> str:
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
[PY, "-c", CAPTURE_CODE], capture_output=True, text=True, check=True
|
|
38
|
+
)
|
|
39
|
+
return result.stdout
|
|
40
|
+
|
|
41
|
+
stdout = benchmark(run)
|
|
42
|
+
assert stdout == _EXPECTED
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_spawn_capture_asyncio_subprocess(benchmark: BenchmarkFixture) -> None:
|
|
46
|
+
# `asyncio.run` opens/closes a fresh event loop per call — the same
|
|
47
|
+
# "naive per-call" shape a caller reaching for asyncio.subprocess without
|
|
48
|
+
# already running inside a long-lived loop would write.
|
|
49
|
+
async def scenario() -> str:
|
|
50
|
+
proc = await asyncio.create_subprocess_exec(
|
|
51
|
+
PY, "-c", CAPTURE_CODE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
|
52
|
+
)
|
|
53
|
+
stdout, _stderr = await proc.communicate()
|
|
54
|
+
assert proc.returncode == 0
|
|
55
|
+
return stdout.decode()
|
|
56
|
+
|
|
57
|
+
def run() -> str:
|
|
58
|
+
return asyncio.run(scenario())
|
|
59
|
+
|
|
60
|
+
stdout = benchmark(run)
|
|
61
|
+
assert stdout == _EXPECTED
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Line-streaming throughput: `RunningProcess.stdout_lines()` (see
|
|
2
|
+
`docs/streaming.md`) draining a child that writes a known number of lines as
|
|
3
|
+
fast as it can, end to end (spawn through `afinish()`).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from processkit import Command
|
|
12
|
+
|
|
13
|
+
from ._shared import PY
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pytest_benchmark.fixture import BenchmarkFixture
|
|
17
|
+
|
|
18
|
+
#: How many lines the child emits per run. Large enough that the loop
|
|
19
|
+
#: dominates over the fixed spawn cost, small enough that a bench round stays
|
|
20
|
+
#: fast (this file runs once per nightly job, at whatever --benchmark-min-rounds
|
|
21
|
+
#: pytest-benchmark's calibration lands on).
|
|
22
|
+
_LINE_COUNT = 5_000
|
|
23
|
+
|
|
24
|
+
_PRODUCER = f"for i in range({_LINE_COUNT}): print(i)"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_stdout_lines_throughput(benchmark: BenchmarkFixture) -> None:
|
|
28
|
+
async def scenario() -> int:
|
|
29
|
+
proc = await Command(PY, ["-c", _PRODUCER]).astart()
|
|
30
|
+
count = 0
|
|
31
|
+
async for _line in proc.stdout_lines():
|
|
32
|
+
count += 1
|
|
33
|
+
finished = await proc.afinish()
|
|
34
|
+
assert finished.exited_zero
|
|
35
|
+
return count
|
|
36
|
+
|
|
37
|
+
def run() -> int:
|
|
38
|
+
return asyncio.run(scenario())
|
|
39
|
+
|
|
40
|
+
lines = benchmark(run)
|
|
41
|
+
assert lines == _LINE_COUNT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# cargo-deny configuration. CI runs
|
|
2
|
+
# `cargo deny check advisories bans licenses sources`
|
|
3
|
+
# (see .github/workflows/ci.yml, `rust-audit` job). Kept in the PR gate (not
|
|
4
|
+
# nightly-hardening.yml) for the same reason
|
|
5
|
+
# pip-audit is: the crate is compiled into every wheel users install, so a
|
|
6
|
+
# newly-disclosed RustSec advisory or license violation should block the next
|
|
7
|
+
# merge, not wait for the nightly schedule. Adapted from the sibling
|
|
8
|
+
# ProcessKit-rs repository's `deny.toml` (same categories), scoped to this
|
|
9
|
+
# binding's own Cargo tree (pyo3, pyo3-async-runtimes, tokio, tracing, ...).
|
|
10
|
+
[advisories]
|
|
11
|
+
# Fail on crates with a RustSec security advisory or that have been yanked.
|
|
12
|
+
version = 2
|
|
13
|
+
yanked = "deny"
|
|
14
|
+
|
|
15
|
+
[licenses]
|
|
16
|
+
# This crate is MIT and is compiled into the distributed wheels; the
|
|
17
|
+
# dependency tree must stay on permissive licenses a downstream MIT/Apache
|
|
18
|
+
# consumer can absorb. A new transitive dependency with anything outside this
|
|
19
|
+
# list (copyleft, source-available, unknown) fails CI loudly instead of
|
|
20
|
+
# slipping into a release.
|
|
21
|
+
version = 2
|
|
22
|
+
allow = [
|
|
23
|
+
"MIT",
|
|
24
|
+
"Apache-2.0",
|
|
25
|
+
# target-lexicon (a pyo3-build-config build-dependency) uses this LLVM
|
|
26
|
+
# linking exception on top of Apache-2.0 -- still permissive, no
|
|
27
|
+
# copyleft/attribution burden beyond plain Apache-2.0.
|
|
28
|
+
"Apache-2.0 WITH LLVM-exception",
|
|
29
|
+
"BSD-3-Clause",
|
|
30
|
+
"Unicode-3.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[bans]
|
|
34
|
+
# Duplicate versions of the same crate are a warning, not a hard failure.
|
|
35
|
+
multiple-versions = "warn"
|
|
36
|
+
# Wildcard version requirements hide breakage; flag them.
|
|
37
|
+
wildcards = "deny"
|
|
38
|
+
|
|
39
|
+
[sources]
|
|
40
|
+
# Only allow dependencies from the official crates.io registry -- no git
|
|
41
|
+
# dependencies or unknown/private registries sneaking into the tree that
|
|
42
|
+
# gets compiled into every wheel.
|
|
43
|
+
unknown-registry = "deny"
|
|
44
|
+
unknown-git = "deny"
|
|
@@ -46,11 +46,13 @@ handy before you ship: it collects every per-OS caveat in one place.
|
|
|
46
46
|
| [Coming from subprocess](migrating.md) | Side-by-side translation of `subprocess` / `asyncio.subprocess` patterns, the exception mapping, and the whole-tree containment the stdlib can't give |
|
|
47
47
|
| [Running commands](commands.md) | The `Command` builder end to end — args, env/sandboxing, stdin, stdout/stderr redirection, encodings, output caps, timeouts, privileges — and every consuming verb (`output`, `run`, `probe`, …) with its error semantics |
|
|
48
48
|
| [Process groups](process-groups.md) | Kill-on-drop containment: creating groups, spawning, teardown, whole-tree signals, suspend/resume, member listing, resource limits, stats |
|
|
49
|
+
| [Sandboxing untrusted tools](sandboxing.md) | The agent/LLM-tool recipe: locked-down env → bounded output → group resource limits → timeout → teardown, a checklist, and an honest threat model (what this does and does not protect against) |
|
|
49
50
|
| [Streaming & interactive I/O](streaming.md) | `astart()` and the live `RunningProcess`: line streaming, interactive stdin, readiness probes (`wait_for_line` / `wait_for_port` / `wait_until`), per-run profiling |
|
|
50
51
|
| [Pipelines](pipelines.md) | Shell-free command pipelines — chain with `.pipe()` or the pipe operator: wiring, pipefail attribution, chain timeouts, binary tails |
|
|
51
52
|
| [Timeouts & cancellation](timeouts-and-cancellation.md) | How a deadline is *captured* vs when it raises, interrupting a blocked sync call (Ctrl+C), and asyncio cancellation that reaps the whole tree |
|
|
52
53
|
| [Supervision](supervision.md) | Keeping a child alive: restart policies, backoff & jitter, stop conditions, outcomes |
|
|
53
54
|
| [Testing your code](testing.md) | The `ProcessRunner` seam — `ScriptedRunner` (incl. scripted streaming `start()`), record/replay cassettes, the `RecordingRunner` spy, the `CliClient` wrapper, and the autoloaded **pytest plugin** (ready-made fixtures + a no-real-spawn guard) |
|
|
55
|
+
| [Command-line usage](cli.md) | `python -m processkit run -- ...`: containment and resource limits for a shell command with no Python to write, flags, exit codes |
|
|
54
56
|
| [Async runtimes & event loops](event-loops.md) | Which event loops the asyncio-native surface runs on — asyncio and uvloop (yes), anyio-on-asyncio (yes), native trio / anyio-on-trio / curio (no) — and why |
|
|
55
57
|
| [Platform support](platforms.md) | The containment mechanisms, every per-feature support matrix in one place, and the caveats worth knowing before you ship |
|
|
56
58
|
|
|
@@ -62,10 +64,11 @@ record/replay, and opt-in logging are all present in every published wheel.
|
|
|
62
64
|
There is nothing to opt into at install time:
|
|
63
65
|
|
|
64
66
|
```bash
|
|
65
|
-
pip install processkit-py #
|
|
67
|
+
pip install processkit-py # import name: processkit
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
On a platform without a prebuilt wheel, build from source (`uv run maturin
|
|
71
|
+
develop`) — see the
|
|
69
72
|
[README](https://github.com/ZelAnton/processkit-py#building-from-source).
|
|
70
73
|
|
|
71
74
|
Distributed as **abi3 wheels for CPython 3.10+** (one wheel per OS/arch runs on
|