processkit-py 1.1.1__tar.gz → 1.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 (126) hide show
  1. {processkit_py-1.1.1 → processkit_py-1.2.1}/CHANGELOG.md +159 -1
  2. {processkit_py-1.1.1 → processkit_py-1.2.1}/CONTRIBUTING.md +5 -0
  3. {processkit_py-1.1.1 → processkit_py-1.2.1}/Cargo.lock +3 -3
  4. {processkit_py-1.1.1 → processkit_py-1.2.1}/Cargo.toml +4 -4
  5. {processkit_py-1.1.1 → processkit_py-1.2.1}/PKG-INFO +13 -2
  6. {processkit_py-1.1.1 → processkit_py-1.2.1}/README.md +12 -1
  7. {processkit_py-1.1.1 → processkit_py-1.2.1}/RELEASING.md +6 -10
  8. {processkit_py-1.1.1 → processkit_py-1.2.1}/SECURITY.md +8 -0
  9. processkit_py-1.2.1/benchmarks/README.md +57 -0
  10. processkit_py-1.2.1/benchmarks/__init__.py +11 -0
  11. processkit_py-1.2.1/benchmarks/_shared.py +20 -0
  12. processkit_py-1.2.1/benchmarks/test_output_all.py +52 -0
  13. processkit_py-1.2.1/benchmarks/test_process_group.py +26 -0
  14. processkit_py-1.2.1/benchmarks/test_spawn_capture.py +61 -0
  15. processkit_py-1.2.1/benchmarks/test_streaming_throughput.py +41 -0
  16. processkit_py-1.2.1/deny.toml +44 -0
  17. {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/README.md +5 -2
  18. {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/api-reference.md +3 -1
  19. processkit_py-1.2.1/docs/cli.md +114 -0
  20. {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/commands.md +116 -4
  21. {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/cookbook.md +75 -14
  22. {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/event-loops.md +4 -4
  23. {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/internals.md +62 -18
  24. {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/migrating.md +1 -1
  25. {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/process-groups.md +15 -0
  26. processkit_py-1.2.1/docs/sandboxing.md +208 -0
  27. {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/streaming.md +124 -34
  28. {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/supervision.md +40 -2
  29. {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/testing.md +1 -1
  30. {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/timeouts-and-cancellation.md +7 -5
  31. processkit_py-1.2.1/examples/04_sandbox_resource_limits.py +98 -0
  32. {processkit_py-1.1.1 → processkit_py-1.2.1}/examples/README.md +4 -2
  33. {processkit_py-1.1.1 → processkit_py-1.2.1}/mkdocs.yml +2 -1
  34. {processkit_py-1.1.1 → processkit_py-1.2.1}/pyproject.toml +52 -3
  35. {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/ci-privileged-guard.py +7 -3
  36. {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/gen_api_reference.py +3 -2
  37. {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/release/cargo_lock.py +4 -1
  38. {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/release/changelog.py +31 -12
  39. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/batch.rs +10 -7
  40. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/cli.rs +36 -2
  41. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/command.rs +203 -50
  42. processkit_py-1.2.1/src/convert.rs +848 -0
  43. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/errors.rs +4 -0
  44. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/group.rs +56 -24
  45. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/lib.rs +8 -2
  46. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/processkit/__init__.py +2 -1
  47. processkit_py-1.2.1/src/processkit/__main__.py +263 -0
  48. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/processkit/_aio.py +101 -11
  49. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/processkit/_processkit.pyi +221 -82
  50. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/processkit/_protocols.py +5 -5
  51. processkit_py-1.2.1/src/processkit/_types.py +117 -0
  52. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/processkit/pytest_plugin.py +7 -0
  53. processkit_py-1.2.1/src/result.rs +867 -0
  54. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/runner.rs +155 -63
  55. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/running.rs +115 -54
  56. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/runtime.rs +1 -8
  57. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/supervisor.rs +209 -8
  58. processkit_py-1.2.1/tests/_liveness.py +195 -0
  59. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/_programs.py +16 -6
  60. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/_typing_pins.py +81 -3
  61. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/property/test_argv_env_roundtrip.py +2 -2
  62. processkit_py-1.2.1/tests/test_batch.py +362 -0
  63. processkit_py-1.2.1/tests/test_ci_privileged_guard.py +102 -0
  64. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_cli_client.py +16 -2
  65. processkit_py-1.2.1/tests/test_cli_main.py +138 -0
  66. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_command.py +691 -4
  67. processkit_py-1.2.1/tests/test_event_loops.py +235 -0
  68. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_hardening.py +8 -0
  69. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_logging.py +20 -1
  70. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_pipelines.py +41 -9
  71. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_pytest_plugin.py +22 -0
  72. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_readiness.py +151 -4
  73. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_release_scripts.py +129 -0
  74. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_streaming.py +174 -0
  75. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_supervisor.py +30 -0
  76. processkit_py-1.2.1/tests/test_threading.py +521 -0
  77. {processkit_py-1.1.1 → processkit_py-1.2.1}/uv.lock +314 -217
  78. processkit_py-1.1.1/examples/04_sandbox_resource_limits.py +0 -82
  79. processkit_py-1.1.1/src/convert.rs +0 -289
  80. processkit_py-1.1.1/src/processkit/_types.py +0 -52
  81. processkit_py-1.1.1/src/result.rs +0 -458
  82. processkit_py-1.1.1/tests/_liveness.py +0 -97
  83. processkit_py-1.1.1/tests/test_batch.py +0 -164
  84. {processkit_py-1.1.1 → processkit_py-1.2.1}/.editorconfig +0 -0
  85. {processkit_py-1.1.1 → processkit_py-1.2.1}/.gitattributes +0 -0
  86. {processkit_py-1.1.1 → processkit_py-1.2.1}/.gitignore +0 -0
  87. {processkit_py-1.1.1 → processkit_py-1.2.1}/.pre-commit-config.yaml +0 -0
  88. {processkit_py-1.1.1 → processkit_py-1.2.1}/.python-version +0 -0
  89. {processkit_py-1.1.1 → processkit_py-1.2.1}/.yamllint.yml +0 -0
  90. {processkit_py-1.1.1 → processkit_py-1.2.1}/LICENSE +0 -0
  91. {processkit_py-1.1.1 → processkit_py-1.2.1}/ROADMAP.md +0 -0
  92. {processkit_py-1.1.1 → processkit_py-1.2.1}/cliff.toml +0 -0
  93. {processkit_py-1.1.1 → processkit_py-1.2.1}/conftest.py +0 -0
  94. {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/pipelines.md +0 -0
  95. {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/platforms.md +0 -0
  96. {processkit_py-1.1.1 → processkit_py-1.2.1}/examples/01_no_orphan_guarantee.py +0 -0
  97. {processkit_py-1.1.1 → processkit_py-1.2.1}/examples/02_wait_for_server.py +0 -0
  98. {processkit_py-1.1.1 → processkit_py-1.2.1}/examples/03_supervise_until_healthy.py +0 -0
  99. {processkit_py-1.1.1 → processkit_py-1.2.1}/rust-toolchain.toml +0 -0
  100. {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/check-env.ps1 +0 -0
  101. {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/check-env.sh +0 -0
  102. {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/ci-privileged-check.sh +0 -0
  103. {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/release/__init__.py +0 -0
  104. {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/smoke.py +0 -0
  105. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/cancellation.rs +0 -0
  106. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/logging.rs +0 -0
  107. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/processkit/py.typed +0 -0
  108. {processkit_py-1.1.1 → processkit_py-1.2.1}/src/processkit/testing.py +0 -0
  109. {processkit_py-1.1.1 → processkit_py-1.2.1}/stubtest-allowlist.txt +0 -0
  110. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/__init__.py +0 -0
  111. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/_docs_snippets.py +0 -0
  112. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/conftest.py +0 -0
  113. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/property/__init__.py +0 -0
  114. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/property/conftest.py +0 -0
  115. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/property/test_numeric_validation.py +0 -0
  116. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/property/test_output_limit.py +0 -0
  117. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/property/test_signals.py +0 -0
  118. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/property/test_wait_for_line.py +0 -0
  119. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_api_reference.py +0 -0
  120. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_api_surface.py +0 -0
  121. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_async.py +0 -0
  122. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_docs_snippets.py +0 -0
  123. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_examples.py +0 -0
  124. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_exceptions.py +0 -0
  125. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_process_group.py +0 -0
  126. {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_runner_seam.py +0 -0
@@ -16,6 +16,162 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
16
  ### Fixed
17
17
  -
18
18
 
19
+ ## [1.2.1] - 2026-07-09
20
+
21
+ ### Added
22
+
23
+ - Add Command.prefer_local, exposing crate 2.2's bare-name resolution override
24
+ - Add Command.prefer_local, exposing crate 2.2's bare-name resolution override
25
+ - Add a runnable Command.prefer_local example to docs
26
+ - Add ProcessStdin.send_control for interactive control-byte delivery
27
+ - Add ProcessStdin.send_control for interactive control-byte delivery
28
+ - Add Command.prefer_local usage example to docs/commands.md
29
+
30
+
31
+ ### Changed
32
+
33
+ - Broaden Command.priority docstring privilege caveat to cover above_normal and niced-parent normal
34
+ - Bump processkit dependency requirement and lockfile to 2.2.0
35
+ - Bump processkit dependency requirement and lockfile to 2.2.0
36
+ - Broaden Command.priority docstring privilege caveat to cover above_normal and niced-parent normal
37
+ - Initialize integration workspace for batch B-20260709T132808Z
38
+ - Initialize integration workspace for batch B-20260709T144637Z
39
+ - Apply rustfmt to send_control signature
40
+ - Bump processkit dependency to 2.2.1
41
+ - Initialize integration workspace for batch B-20260709T162529Z
42
+ - Bump processkit dependency to 2.2.1
43
+
44
+
45
+ ### Fixed
46
+
47
+ - Fix Windows-incompatible relative path-form assertion in the prefer_local example
48
+
49
+ ## [1.2.0] - 2026-07-08
50
+
51
+ ### Added
52
+ - `Command.stdout_tee` / `Command.stderr_tee` now accept a **Python writer**
53
+ object (anything with a callable `write()` — `io.StringIO`, `sys.stderr`, a
54
+ text-mode file, a logger wrapper) in addition to a file path, mirroring the
55
+ child's output straight into your own console/buffer/logger while still
56
+ capturing it. Each decoded line (plus a `"\n"`) is passed to `write()` as a
57
+ `str` via an async-write bridge: every write is dispatched to the runtime's
58
+ blocking pool (re-acquiring the GIL there) and awaited on the capture pump, so
59
+ a slow — even sleeping — `write()` applies backpressure without blocking the
60
+ event loop or deadlocking the runtime. The object is discriminated from a path
61
+ by exposing `write` (neither `str` nor `pathlib.Path` does) and is never closed
62
+ for you; `append=True` is meaningful only for a file path and raises
63
+ `ValueError` if combined with a writer. A `write()` exception disables the tee
64
+ for the rest of the run (a `tracing` warning under `enable_logging()`, the same
65
+ isolation as the file tee) and is additionally reported via `sys.unraisablehook`
66
+ — the run and its captured result are unaffected. The previous "a file path
67
+ only, an arbitrary Python writer is deliberately not supported" restriction is
68
+ lifted. See `docs/streaming.md#tee-output-to-a-file`.
69
+ - `Command.on_stdout_line(callback)` / `Command.on_stderr_line(callback)`: a
70
+ `Callable[[str], None]` invoked with every decoded line as it is produced —
71
+ the way to give the **synchronous** surface (`.output()`/`.run()`) live
72
+ progress observation during an otherwise-blocking call, without losing the
73
+ full capture. Also fires on the async verbs and on a streamed run
74
+ (`start()`/`astart()` + `stdout_lines()`/`output_events()`); at most one
75
+ handler per stream (a repeat call replaces the previous one); a raising
76
+ callback is reported via `sys.unraisablehook` rather than propagated or
77
+ breaking the run. Inert under `stdout("inherit")`/`stdout("null")` (resp.
78
+ `stderr(...)`) and, for `on_stdout_line` only, under `output_bytes()` (which
79
+ captures stdout raw, bypassing the line pump — stderr still goes through it,
80
+ so `on_stderr_line` still fires there). See
81
+ `docs/streaming.md#live-per-line-callbacks`.
82
+ - A `benchmarks/` suite (`pytest-benchmark`, new `bench` dependency-group)
83
+ measuring spawn+capture overhead against `subprocess`/`asyncio.subprocess`,
84
+ `ProcessGroup` start/exit, line-streaming throughput, and `output_all`
85
+ concurrency scaling — dev tooling only, no public API change. Runs nightly
86
+ via the `bench` job in `nightly-hardening.yml`, never in the PR gate; see
87
+ `benchmarks/README.md`.
88
+ - `wait_for_path(path, *, timeout, interval=0.05)` — a new async readiness
89
+ helper alongside `wait_until` / `wait_for_port` / `wait_for_line`, polling
90
+ until a filesystem path appears (a unix socket, a pid file, or any other
91
+ marker a daemon creates once ready). Same timeout/interval discipline as its
92
+ siblings (NaN/negative `timeout` and non-positive `interval` raise
93
+ `ValueError`; `timeout=0` still checks the path at least once) and raises
94
+ `WaitTimeout` (also a `TimeoutError`) on expiry, now carrying a `path` field
95
+ (`WaitTimeout.__init__` gained a `path: StrPath | None = None` parameter).
96
+ - `python -m processkit run -- <cmd> [args...]`: a CLI wrapper that runs a
97
+ command inside a kill-on-exit `ProcessGroup` with inherited stdio, for
98
+ shell scripts and CI steps with no Python to write. Supports `--timeout`,
99
+ `--timeout-grace`, `--max-memory`, `--max-processes`, and `--cpu-quota`;
100
+ the child's own exit code is passed through unchanged,
101
+ and a timeout / missing program / rejected resource limit is reported as a
102
+ one-line stderr message with a documented, GNU-`timeout`-style exit code
103
+ instead of a traceback. See `docs/cli.md`.
104
+ - `Finished` gains `timed_out` and `signal` properties that delegate to the
105
+ nested `outcome`, so it now mirrors `Outcome` fully — matching `code` and
106
+ `exited_zero`, which were already exposed directly — instead of requiring
107
+ `finished.outcome.timed_out` / `finished.outcome.signal`.
108
+ - `Command.stdin_file(path)` — feed the child's stdin from a file, streamed in
109
+ chunks by the crate rather than read whole into a Python `bytes` object, for
110
+ large inputs (a `psql` dump, a `tar` archive, a multi-gigabyte log). Like
111
+ most other builder methods (`stdout_tee`/`stderr_tee` are the deliberate
112
+ exception), it does not touch the filesystem at build time — the path is
113
+ opened lazily at spawn, so a missing/unreadable file surfaces as the generic
114
+ `ProcessError` from the run/output verb, not `FileNotFoundError`. Reusable
115
+ across retries/re-runs, like `stdin_bytes`/`stdin_text`; the usual "last
116
+ stdin method wins" rule applies alongside `stdin_bytes()`/`stdin_text()`/
117
+ `keep_stdin_open()`.
118
+ - `ProcessResult` and `BytesResult` gain `diagnostic: str | None` (stderr if it
119
+ carries text, otherwise stdout, otherwise `None` — the same preference order
120
+ as `NonZeroExit`/`Timeout`/`Signalled.diagnostic` on the exceptions) and
121
+ `outcome: Outcome` (the same value `RunProfile.outcome` and the checking-verb
122
+ exceptions expose). A result held as data (`output()`/`output_bytes()`
123
+ without `ensure_success()`) no longer requires re-deriving these by hand.
124
+ (An `output_contains_any` convenience was considered alongside these and
125
+ rejected: the underlying `processkit` crate has no such method, so it
126
+ wouldn't be parity with the crate or the exceptions like `diagnostic`/
127
+ `outcome` are — and it's a one-liner callers can already write themselves
128
+ via `combined`, e.g. `any(s in result.combined for s in needles)`.)
129
+ - Value semantics for the result types: `ProcessResult`, `BytesResult`,
130
+ `Outcome`, `Finished`, `RunProfile`, and `SupervisionOutcome` now define
131
+ `__eq__` (comparing every field the underlying `processkit` crate's own
132
+ `PartialEq` compares — not `object`'s previous identity comparison) and a
133
+ consistent `__hash__` (none of their fields are stored floats, so hashing is
134
+ sound), so two results can now be compared with `==` and used in a `set` or
135
+ as a `dict` key without a manual field-by-field comparison.
136
+ `ProcessResult`/`Outcome`/`Finished`/`SupervisionOutcome` are also picklable
137
+ — e.g. to return a `ProcessResult` from a
138
+ `concurrent.futures.ProcessPoolExecutor` worker. The underlying crate has no
139
+ public constructor for any of these types, so unpickling reconstructs one via
140
+ `processkit.testing.ScriptedRunner` (an in-memory, no-subprocess replay) —
141
+ faithful for every field the Python binding exposes, but a command that
142
+ customized `success_codes()`/`timeout()` is not guaranteed to compare `==`
143
+ its original after a round trip (those two fields have no Python accessor to
144
+ reconstruct exactly). `BytesResult` (raw stdout may not be valid UTF-8, and
145
+ the only reconstruction channel available is text-only) and `RunProfile`
146
+ (reports live OS resource-sampling telemetry with no synthesis path outside
147
+ an actual monitored run) explicitly do **not** support pickling and raise a
148
+ clear `TypeError` rather than failing silently or fabricating the missing
149
+ data.
150
+
151
+ ### Changed
152
+ - `CliClient(default_env_fn=...)` now validates that every value in the
153
+ mapping is callable **at construction time**, raising `TypeError` (naming
154
+ the offending key) immediately instead of silently accepting a non-callable
155
+ value and only discovering the mistake later — once per built command, as
156
+ an unraisable-hook warning plus an always-empty resolved env var. Valid
157
+ callables behave exactly as before.
158
+
159
+ ### Fixed
160
+ - `Args` (`from processkit import Args`) no longer rejects the single most
161
+ common real call site — a variable annotated `list[str]` (or
162
+ `list[pathlib.Path]` / `list[os.PathLike[str]]`) passed straight through to
163
+ an argv-like parameter, e.g. `args: list[str] = [...]; cmd.args(args)`.
164
+ `list` is invariant, so the original `list[StrPath] | tuple[StrPath, ...]`
165
+ spelling only ever accepted a `list[StrPath]`-annotated variable or a
166
+ literal, not a `list[str]`/`list[Path]`/`list[os.PathLike[str]]`-annotated
167
+ one, even though the values are runtime-identical — a static-typing-only
168
+ false positive with no runtime effect. `Args` is now a union of the concrete
169
+ homogeneous list shapes (`list[str]`, `list[Path]`, `list[os.PathLike[str]]`)
170
+ instead of the single invariant `list[StrPath]`; a *mixed* `str`/
171
+ `os.PathLike[str]` argv is still accepted, now spelled as a `tuple` rather
172
+ than a `list` literal (e.g. `cmd.args((path, "literal"))`). A bare `str`
173
+ still does not type-check as `Args` (unchanged; see the `Args` docstring).
174
+
19
175
  ## [1.1.1] - 2026-07-06
20
176
 
21
177
  ### Added
@@ -709,7 +865,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
709
865
  - `enable_logging()` enables the crate's `tracing` feature; the bridge pulls
710
866
  `tracing` / `tracing-subscriber` (registry only) into the compiled wheel.
711
867
 
712
- [Unreleased]: https://github.com/ZelAnton/processkit-py/compare/v1.1.1...HEAD
868
+ [Unreleased]: https://github.com/ZelAnton/processkit-py/compare/v1.2.1...HEAD
869
+ [1.2.1]: https://github.com/ZelAnton/processkit-py/compare/v1.2.0...v1.2.1
870
+ [1.2.0]: https://github.com/ZelAnton/processkit-py/compare/v1.1.1...v1.2.0
713
871
  [1.1.1]: https://github.com/ZelAnton/processkit-py/compare/v1.1.0...v1.1.1
714
872
  [1.1.0]: https://github.com/ZelAnton/processkit-py/compare/v1.0.0...v1.1.0
715
873
  [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.0"
168
+ version = "2.2.1"
169
169
  source = "registry+https://github.com/rust-lang/crates.io-index"
170
- checksum = "d4f13810e1562b13246da25b9a00eb2a3da2fd1159c771d21c2328652cfa067d"
170
+ checksum = "d5291d099b0b4780f8fdf0e8d9c9f1308f61dd5562bd22115e8d054576eb23bb"
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.1.1"
187
+ version = "1.2.1"
188
188
  dependencies = [
189
189
  "processkit",
190
190
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "processkit-py"
3
- version = "1.1.1"
3
+ version = "1.2.1"
4
4
  edition = "2021"
5
5
  description = "PyO3 binding crate for the processkit Python package (compiled into wheels)"
6
6
  license = "MIT"
@@ -34,10 +34,10 @@ pyo3 = { version = "0.29", features = ["abi3-py310"] }
34
34
  # runtime we reuse for BOTH the sync surface (`block_on`) and the async surface
35
35
  # (`future_into_py`), so the binding owns exactly one runtime. Tracks pyo3 0.29.
36
36
  pyo3-async-runtimes = { version = "0.29", features = ["tokio-runtime"] }
37
- # The Rust core. Compatible-version requirement `2.1` (>=2.1.0, <3.0.0) — the
37
+ # The Rust core. Compatible-version requirement `2.2` (>=2.2.0, <3.0.0) — the
38
38
  # binding tracks API/behaviour churn deliberately, not transitively. 1.3.0 (a
39
39
  # semver-broken minor) and 2.0.0 (a mistaken publish) were yanked upstream, so
40
- # 2.1.0 is the floor; the requirement stays within the 2.x major on purpose.
40
+ # 2.2.0 is the floor; the requirement stays within the 2.x major on purpose.
41
41
  #
42
42
  # Features are listed EXPLICITLY with `default-features = false`, so a change to
43
43
  # the crate's default set can never silently alter our public surface:
@@ -53,7 +53,7 @@ pyo3-async-runtimes = { version = "0.29", features = ["tokio-runtime"] }
53
53
  # tests; it has no Python value (the Python test doubles come from the crate's
54
54
  # always-on `doubles` surface — ScriptedRunner / RecordingRunner — plus the
55
55
  # `record`-gated cassette RecordReplayRunner enabled above).
56
- processkit = { version = "2.1", default-features = false, features = [
56
+ processkit = { version = "2.2", default-features = false, features = [
57
57
  "process-control",
58
58
  "limits",
59
59
  "record",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: processkit-py
3
- Version: 1.1.1
3
+ Version: 1.2.1
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
- > **Status: 1.0 — API frozen.** The public API follows
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
- > **Status: 1.0 — API frozen.** The public API follows
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 via `.github/workflows/docs.yml`. The build (`mkdocs build --strict`) runs on
72
- every docs change as a link/anchor check; **deployment is opt-in** so there are no
73
- red runs before Pages is set up. To publish the site:
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
- 1. Repo **Settings Pages Source: GitHub Actions**.
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"