processkit-py 1.0.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.0.0/.editorconfig +49 -0
- processkit_py-1.0.0/.gitattributes +11 -0
- processkit_py-1.0.0/.gitignore +89 -0
- processkit_py-1.0.0/.pre-commit-config.yaml +36 -0
- processkit_py-1.0.0/.python-version +1 -0
- processkit_py-1.0.0/.yamllint.yml +34 -0
- processkit_py-1.0.0/CHANGELOG.md +291 -0
- processkit_py-1.0.0/CONTRIBUTING.md +87 -0
- processkit_py-1.0.0/Cargo.lock +525 -0
- processkit_py-1.0.0/Cargo.toml +70 -0
- processkit_py-1.0.0/LICENSE +21 -0
- processkit_py-1.0.0/PKG-INFO +490 -0
- processkit_py-1.0.0/README.md +456 -0
- processkit_py-1.0.0/RELEASING.md +96 -0
- processkit_py-1.0.0/ROADMAP.md +236 -0
- processkit_py-1.0.0/SECURITY.md +41 -0
- processkit_py-1.0.0/cliff.toml +45 -0
- processkit_py-1.0.0/docs/README.md +112 -0
- processkit_py-1.0.0/docs/commands.md +366 -0
- processkit_py-1.0.0/docs/cookbook.md +479 -0
- processkit_py-1.0.0/docs/migrating.md +185 -0
- processkit_py-1.0.0/docs/pipelines.md +179 -0
- processkit_py-1.0.0/docs/platforms.md +81 -0
- processkit_py-1.0.0/docs/process-groups.md +282 -0
- processkit_py-1.0.0/docs/streaming.md +323 -0
- processkit_py-1.0.0/docs/supervision.md +192 -0
- processkit_py-1.0.0/docs/testing.md +278 -0
- processkit_py-1.0.0/docs/timeouts-and-cancellation.md +226 -0
- processkit_py-1.0.0/examples/01_no_orphan_guarantee.py +52 -0
- processkit_py-1.0.0/examples/02_wait_for_server.py +98 -0
- processkit_py-1.0.0/examples/03_supervise_until_healthy.py +59 -0
- processkit_py-1.0.0/examples/04_sandbox_resource_limits.py +82 -0
- processkit_py-1.0.0/examples/README.md +24 -0
- processkit_py-1.0.0/mkdocs.yml +66 -0
- processkit_py-1.0.0/pyproject.toml +138 -0
- processkit_py-1.0.0/rust-toolchain.toml +5 -0
- processkit_py-1.0.0/scripts/check-env.ps1 +64 -0
- processkit_py-1.0.0/scripts/check-env.sh +55 -0
- processkit_py-1.0.0/scripts/smoke.py +23 -0
- processkit_py-1.0.0/src/batch.rs +133 -0
- processkit_py-1.0.0/src/cli.rs +121 -0
- processkit_py-1.0.0/src/command.rs +492 -0
- processkit_py-1.0.0/src/convert.rs +139 -0
- processkit_py-1.0.0/src/errors.rs +202 -0
- processkit_py-1.0.0/src/group.rs +269 -0
- processkit_py-1.0.0/src/lib.rs +104 -0
- processkit_py-1.0.0/src/logging.rs +110 -0
- processkit_py-1.0.0/src/processkit/__init__.py +111 -0
- processkit_py-1.0.0/src/processkit/_aio.py +175 -0
- processkit_py-1.0.0/src/processkit/_processkit.pyi +658 -0
- processkit_py-1.0.0/src/processkit/_runner.py +46 -0
- processkit_py-1.0.0/src/processkit/_types.py +17 -0
- processkit_py-1.0.0/src/processkit/py.typed +0 -0
- processkit_py-1.0.0/src/processkit/testing.py +30 -0
- processkit_py-1.0.0/src/result.rs +407 -0
- processkit_py-1.0.0/src/runner.rs +535 -0
- processkit_py-1.0.0/src/running.rs +383 -0
- processkit_py-1.0.0/src/runtime.rs +87 -0
- processkit_py-1.0.0/src/supervisor.rs +231 -0
- processkit_py-1.0.0/stubtest-allowlist.txt +38 -0
- processkit_py-1.0.0/tests/__init__.py +0 -0
- processkit_py-1.0.0/tests/_liveness.py +94 -0
- processkit_py-1.0.0/tests/_programs.py +28 -0
- processkit_py-1.0.0/tests/_typing_pins.py +140 -0
- processkit_py-1.0.0/tests/test_api_surface.py +277 -0
- processkit_py-1.0.0/tests/test_async.py +138 -0
- processkit_py-1.0.0/tests/test_batch.py +72 -0
- processkit_py-1.0.0/tests/test_cli_client.py +69 -0
- processkit_py-1.0.0/tests/test_command.py +427 -0
- processkit_py-1.0.0/tests/test_examples.py +45 -0
- processkit_py-1.0.0/tests/test_exceptions.py +147 -0
- processkit_py-1.0.0/tests/test_hardening.py +57 -0
- processkit_py-1.0.0/tests/test_import.py +10 -0
- processkit_py-1.0.0/tests/test_logging.py +41 -0
- processkit_py-1.0.0/tests/test_pipelines.py +116 -0
- processkit_py-1.0.0/tests/test_process_group.py +221 -0
- processkit_py-1.0.0/tests/test_readiness.py +316 -0
- processkit_py-1.0.0/tests/test_runner_seam.py +377 -0
- processkit_py-1.0.0/tests/test_streaming.py +353 -0
- processkit_py-1.0.0/tests/test_supervisor.py +152 -0
- processkit_py-1.0.0/uv.lock +584 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
root = true
|
|
2
|
+
|
|
3
|
+
[*]
|
|
4
|
+
charset = utf-8
|
|
5
|
+
end_of_line = lf
|
|
6
|
+
insert_final_newline = true
|
|
7
|
+
trim_trailing_whitespace = true
|
|
8
|
+
indent_style = space
|
|
9
|
+
indent_size = 4
|
|
10
|
+
tab_width = 4
|
|
11
|
+
|
|
12
|
+
# Python — 4-space indent (PEP 8). `ruff format` is the source of truth for .py
|
|
13
|
+
# formatting; this just keeps editors aligned before it runs.
|
|
14
|
+
[*.py]
|
|
15
|
+
indent_style = space
|
|
16
|
+
indent_size = 4
|
|
17
|
+
|
|
18
|
+
# Config files (pyproject.toml, JSON). YAML has its own section below.
|
|
19
|
+
[*.{toml,json}]
|
|
20
|
+
indent_style = space
|
|
21
|
+
indent_size = 2
|
|
22
|
+
tab_width = 2
|
|
23
|
+
|
|
24
|
+
# Windows batch files must have CRLF — CMD will not parse them otherwise.
|
|
25
|
+
[*.{bat,cmd}]
|
|
26
|
+
end_of_line = crlf
|
|
27
|
+
tab_width = 2
|
|
28
|
+
|
|
29
|
+
# PowerShell (the init scripts) — spaces, LF.
|
|
30
|
+
[*.{ps1,psm1,psd1}]
|
|
31
|
+
indent_style = space
|
|
32
|
+
indent_size = 4
|
|
33
|
+
tab_width = 4
|
|
34
|
+
|
|
35
|
+
# Shell scripts (init.sh, check-env.sh) must stay LF.
|
|
36
|
+
[*.sh]
|
|
37
|
+
indent_style = space
|
|
38
|
+
indent_size = 2
|
|
39
|
+
tab_width = 2
|
|
40
|
+
|
|
41
|
+
# YAML cannot use tabs.
|
|
42
|
+
[*.{yaml,yml}]
|
|
43
|
+
indent_style = space
|
|
44
|
+
indent_size = 2
|
|
45
|
+
tab_width = 2
|
|
46
|
+
|
|
47
|
+
# Markdown — keep trailing whitespace (hard line breaks).
|
|
48
|
+
[*.md]
|
|
49
|
+
trim_trailing_whitespace = false
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Default: normalize line endings to LF in the repository
|
|
2
|
+
* text=auto eol=lf
|
|
3
|
+
|
|
4
|
+
# Windows batch files must have CRLF — CMD will not parse them otherwise
|
|
5
|
+
*.cmd text eol=crlf
|
|
6
|
+
*.bat text eol=crlf
|
|
7
|
+
|
|
8
|
+
# PowerShell scripts work with both, but keep LF consistent with the repo default
|
|
9
|
+
*.ps1 text eol=lf
|
|
10
|
+
*.psm1 text eol=lf
|
|
11
|
+
*.psd1 text eol=lf
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
## -------
|
|
2
|
+
## Python build artifacts and caches
|
|
3
|
+
## -------
|
|
4
|
+
/dist/
|
|
5
|
+
/build/
|
|
6
|
+
/artifacts/
|
|
7
|
+
/site/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.py[cod]
|
|
11
|
+
|
|
12
|
+
# Virtual environments (uv creates .venv). uv.lock is NOT ignored — commit it.
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
|
|
16
|
+
# Tool caches
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
.coverage
|
|
21
|
+
.coverage.*
|
|
22
|
+
htmlcov/
|
|
23
|
+
.tox/
|
|
24
|
+
|
|
25
|
+
# Release pipeline scratch file — written by .github/workflows/release.yml
|
|
26
|
+
release-notes.md
|
|
27
|
+
|
|
28
|
+
# CI scratch — exported lockfile the pip-audit job (.github/workflows/ci.yml) scans.
|
|
29
|
+
requirements-audit.txt
|
|
30
|
+
|
|
31
|
+
## -------
|
|
32
|
+
## Editors / IDEs
|
|
33
|
+
## -------
|
|
34
|
+
.vs/
|
|
35
|
+
.vscode/
|
|
36
|
+
!.vscode/extensions.json
|
|
37
|
+
!.vscode/settings.json
|
|
38
|
+
!.vscode/tasks.json
|
|
39
|
+
!.vscode/launch.json
|
|
40
|
+
*.code-workspace
|
|
41
|
+
.history/
|
|
42
|
+
.idea/
|
|
43
|
+
|
|
44
|
+
## -------
|
|
45
|
+
## Secrets / local config
|
|
46
|
+
## -------
|
|
47
|
+
.env
|
|
48
|
+
.env.local
|
|
49
|
+
|
|
50
|
+
## -------
|
|
51
|
+
## macOS
|
|
52
|
+
## -------
|
|
53
|
+
.DS_Store
|
|
54
|
+
.AppleDouble
|
|
55
|
+
.LSOverride
|
|
56
|
+
._*
|
|
57
|
+
.Spotlight-V100
|
|
58
|
+
.Trashes
|
|
59
|
+
|
|
60
|
+
## -------
|
|
61
|
+
## Windows
|
|
62
|
+
## -------
|
|
63
|
+
Thumbs.db
|
|
64
|
+
Thumbs.db:encryptable
|
|
65
|
+
ehthumbs.db
|
|
66
|
+
ehthumbs_vista.db
|
|
67
|
+
Desktop.ini
|
|
68
|
+
$RECYCLE.BIN/
|
|
69
|
+
*.lnk
|
|
70
|
+
|
|
71
|
+
## -------
|
|
72
|
+
## Agent-instruction files — kept on disk for tooling but never pushed to remote.
|
|
73
|
+
## CLAUDE.md, CLAUDE.local.md, AGENTS.md, and .claude/ are local-only here.
|
|
74
|
+
## -------
|
|
75
|
+
/CLAUDE.md
|
|
76
|
+
/CLAUDE.local.md
|
|
77
|
+
/AGENTS.md
|
|
78
|
+
.claude/
|
|
79
|
+
|
|
80
|
+
## -------
|
|
81
|
+
## Rust build artifacts
|
|
82
|
+
## -------
|
|
83
|
+
/target/
|
|
84
|
+
|
|
85
|
+
# Compiled PyO3 extension (installed by `maturin develop` into the Python package dir).
|
|
86
|
+
# The .pyd / .so / .pdb are build outputs, not source.
|
|
87
|
+
src/processkit/_processkit*.pyd
|
|
88
|
+
src/processkit/_processkit*.so
|
|
89
|
+
src/processkit/_processkit*.pdb
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Optional but recommended local gate. Install with:
|
|
2
|
+
# uv run pre-commit install
|
|
3
|
+
# Run against everything with:
|
|
4
|
+
# uv run pre-commit run --all-files
|
|
5
|
+
# Keep hook versions current with `uv run pre-commit autoupdate`.
|
|
6
|
+
#
|
|
7
|
+
# These mirror the CI gates (ruff, ruff-format, cargo fmt). `cargo clippy` and
|
|
8
|
+
# the test suite are intentionally left to CI — too slow for a commit hook.
|
|
9
|
+
repos:
|
|
10
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
11
|
+
rev: v5.0.0
|
|
12
|
+
hooks:
|
|
13
|
+
- id: trailing-whitespace
|
|
14
|
+
- id: end-of-file-fixer
|
|
15
|
+
- id: check-yaml
|
|
16
|
+
- id: check-toml
|
|
17
|
+
- id: check-merge-conflict
|
|
18
|
+
- id: check-added-large-files
|
|
19
|
+
|
|
20
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
21
|
+
rev: v0.15.20
|
|
22
|
+
hooks:
|
|
23
|
+
# Lint (with --fix) before formatting, per ruff's guidance: a fix can emit
|
|
24
|
+
# code that then needs reformatting.
|
|
25
|
+
- id: ruff-check
|
|
26
|
+
args: [--fix]
|
|
27
|
+
- id: ruff-format
|
|
28
|
+
|
|
29
|
+
- repo: local
|
|
30
|
+
hooks:
|
|
31
|
+
- id: cargo-fmt
|
|
32
|
+
name: cargo fmt
|
|
33
|
+
entry: cargo fmt --all --
|
|
34
|
+
language: system
|
|
35
|
+
types: [rust]
|
|
36
|
+
pass_filenames: false
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# yamllint configuration, tuned for GitHub Actions workflows.
|
|
2
|
+
#
|
|
3
|
+
# The default yamllint preset is calibrated for hand-written config, not CI
|
|
4
|
+
# workflows: it caps lines at 80 columns and flags the workflow `on:` key as a
|
|
5
|
+
# YAML 1.1 boolean. The overrides below keep the checks that catch real defects
|
|
6
|
+
# (tabs, duplicate keys, bad indentation, trailing whitespace) while silencing
|
|
7
|
+
# the rules that fight normal Actions YAML.
|
|
8
|
+
#
|
|
9
|
+
# Run locally with: uvx yamllint . (or: yamllint .)
|
|
10
|
+
|
|
11
|
+
extends: default
|
|
12
|
+
|
|
13
|
+
rules:
|
|
14
|
+
# Long `run:` commands, URLs, and ${{ ... }} expressions are routine in
|
|
15
|
+
# workflows and often can't be wrapped. Keep the check as a non-failing
|
|
16
|
+
# warning at a realistic width instead of an error at 80.
|
|
17
|
+
line-length:
|
|
18
|
+
max: 120
|
|
19
|
+
level: warning
|
|
20
|
+
allow-non-breakable-words: true
|
|
21
|
+
|
|
22
|
+
# `on:` is a required GitHub Actions key, but YAML 1.1 reads it as the boolean
|
|
23
|
+
# `true`. Don't check keys against the truthy rule (values are still checked).
|
|
24
|
+
truthy:
|
|
25
|
+
check-keys: false
|
|
26
|
+
|
|
27
|
+
# Workflows don't use a `---` document-start marker.
|
|
28
|
+
document-start: disable
|
|
29
|
+
|
|
30
|
+
# Allow a single space before inline comments, e.g. the `# v6` version note on
|
|
31
|
+
# a SHA-pinned action. (Default requires two.)
|
|
32
|
+
comments:
|
|
33
|
+
min-spaces-from-content: 1
|
|
34
|
+
comments-indentation: disable
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **processkit** are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
-
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
-
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
-
|
|
18
|
+
|
|
19
|
+
## [1.0.0] - 2026-07-04
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- Synchronous `Command` builder over the `processkit` Rust crate (pinned at
|
|
23
|
+
`=1.2.0`): `output()` (captures a non-zero exit, timeout, and signal-kill as
|
|
24
|
+
data), `output_bytes()` (raw-bytes stdout → `BytesResult`), `run()` (returns
|
|
25
|
+
trimmed stdout, raises on failure), `exit_code()`, and `probe()`, configured
|
|
26
|
+
with `arg`/`args`/`cwd`/`env`/`envs`/`env_remove`/`env_clear`/`timeout`/
|
|
27
|
+
`output_limit`. The program and working directory accept any `os.PathLike`, not
|
|
28
|
+
only `str`.
|
|
29
|
+
- Full environment control on `Command`: `envs(mapping)` (set many at once),
|
|
30
|
+
`env_remove(key)`, and `env_clear()` (start from an empty environment) — for
|
|
31
|
+
reproducible or locked-down (sandboxed) children.
|
|
32
|
+
- Output caps on `Command`: `output_limit(max_bytes=…, max_lines=…,
|
|
33
|
+
on_overflow="drop_oldest"|"drop_newest"|"error")` bounds how much captured
|
|
34
|
+
output is retained (cap `max_bytes` to bound the parent's memory against an
|
|
35
|
+
untrusted child; a `max_lines`-only cap does not); on `"error"` overflow the
|
|
36
|
+
run raises `OutputTooLarge`.
|
|
37
|
+
- More `Command` knobs: `success_codes([…])` (treat the given exit codes as
|
|
38
|
+
success, replacing the default `{0}` — for `grep`/`diff`-style tools),
|
|
39
|
+
`inherit_env([…])`
|
|
40
|
+
(allowlist inheritance), `timeout_grace()` / `timeout_signal()` (graceful
|
|
41
|
+
timeout), `stdout("inherit"|"null")` / `stderr(…)` redirection, `encoding(…)` /
|
|
42
|
+
`stdout_encoding` / `stderr_encoding` (decode non-UTF-8 output),
|
|
43
|
+
`kill_on_parent_death()`, `create_no_window()` (Windows), and POSIX
|
|
44
|
+
`uid` / `gid` / `groups` / `setsid`.
|
|
45
|
+
- Concurrent batch execution: `output_all` / `aoutput_all` (and `…_bytes`
|
|
46
|
+
variants) run many commands with bounded `concurrency`, returning each
|
|
47
|
+
`ProcessResult` — or a `ProcessError` for a spawn/I/O failure — in input order.
|
|
48
|
+
- `CliClient(program, *, default_timeout=…, default_env=…, default_env_remove=…)`
|
|
49
|
+
— a typed wrapper for a tool you call repeatedly, with `run` / `output` /
|
|
50
|
+
`output_bytes` / `exit_code` / `probe` (+ async) taking just the per-call args.
|
|
51
|
+
- `enable_logging()` — opt-in observability: forwards the core's per-run events to
|
|
52
|
+
Python's `logging` (a `processkit` logger; DEBUG for a run, WARNING for an edge
|
|
53
|
+
case). Idempotent; off by default; `argv`/`env` are never logged (secrets). Use
|
|
54
|
+
`logging.basicConfig(level=…)` and filter the `processkit` logger as usual.
|
|
55
|
+
- `RunningProcess` live introspection (`elapsed_seconds`, `cpu_time_seconds`,
|
|
56
|
+
`peak_memory_bytes`, `stdout_line_count` / `stderr_line_count`, `owns_group`),
|
|
57
|
+
plus `output_bytes()` and `profile(every_seconds)` → `RunProfile`. A `RunProfile`
|
|
58
|
+
carries the run's full `outcome` (`code` / `signal` / `timed_out` — a superset of
|
|
59
|
+
`wait()`) alongside the CPU/memory samples (`cpu_time_seconds`,
|
|
60
|
+
`peak_memory_bytes`, `avg_cpu_cores`, `samples`).
|
|
61
|
+
- Synchronous `Command.start()` — a blocking twin of `astart()` returning a live
|
|
62
|
+
`RunningProcess` for streaming a child from synchronous code (its consuming
|
|
63
|
+
methods `wait` / `finish` / `output` / … remain coroutines, awaited from an
|
|
64
|
+
event loop).
|
|
65
|
+
- `RecordReplayRunner` test double — `record(path)` real runs then `save()`, and
|
|
66
|
+
`replay(path)` offline; plus `output_bytes` on `Runner` / `ScriptedRunner`. It
|
|
67
|
+
records and replays the streaming `start()` verb too (record is capture-whole;
|
|
68
|
+
interactive mid-stream stdin can't be cassette-recorded — script those with
|
|
69
|
+
`ScriptedRunner`); `output_bytes` through a cassette raises `Unsupported` (a
|
|
70
|
+
text fixture can't reproduce exact bytes).
|
|
71
|
+
- `RecordingRunner` spy test double — `RecordingRunner.replying(reply)` answers
|
|
72
|
+
every command with one canned `Reply` and records each call, so a test can
|
|
73
|
+
assert on *what* its code ran: `calls()` returns every `Invocation` (in order)
|
|
74
|
+
and `only_call()` the single one. Each `Invocation` exposes `program`, `args`,
|
|
75
|
+
`cwd`, `env`, `has_stdin`, and `has_flag(flag)`; its `repr` is redacted (program
|
|
76
|
+
+ arg count + env names, never values). Completes the test-double set.
|
|
77
|
+
- `ProcessResult` with `stdout`, `stderr`, `code`, `is_success`, `timed_out`,
|
|
78
|
+
`signal`, `program`, `duration_seconds`, `truncated`, and `combined`; plus a
|
|
79
|
+
`BytesResult` (raw-bytes `stdout`, text `stderr`) from `output_bytes()` /
|
|
80
|
+
`aoutput_bytes()`.
|
|
81
|
+
- `ProcessGroup` context manager — a kill-on-drop container for a process tree;
|
|
82
|
+
`start()` a command into it, inspect `mechanism` / `members()`, and the whole
|
|
83
|
+
tree (grandchildren included) is reaped on `with`-exit or `shutdown()`.
|
|
84
|
+
- `RunningProcess` handle exposing the child `pid`.
|
|
85
|
+
- Exception hierarchy rooted at `ProcessError`: `NonZeroExit`, `Timeout`,
|
|
86
|
+
`Signalled`, `ProcessNotFound`, `PermissionDenied`, `Unsupported`,
|
|
87
|
+
`OutputTooLarge`. `Timeout` is also a builtin `TimeoutError`, `ProcessNotFound`
|
|
88
|
+
is also a `FileNotFoundError`, and `PermissionDenied` is also a
|
|
89
|
+
`PermissionError` (matching `asyncio` / `subprocess`), so the stdlib `except`
|
|
90
|
+
clauses catch them. The data-carrying ones expose structured fields — e.g.
|
|
91
|
+
`NonZeroExit.code` / `.stdout` / `.stderr` / `.program`,
|
|
92
|
+
`Timeout.timeout_seconds`, `Signalled.signal`, `OutputTooLarge.max_bytes` /
|
|
93
|
+
`.total_bytes`, `Unsupported.operation` — so a failure can be inspected
|
|
94
|
+
programmatically, not just read as a message. (`ResourceLimit` carries no extra
|
|
95
|
+
field; its reason is `str(exc)`.)
|
|
96
|
+
- Blocking synchronous calls are interruptible: `Ctrl+C` (SIGINT) raises
|
|
97
|
+
`KeyboardInterrupt` promptly and tears down the run's process tree, instead of
|
|
98
|
+
hanging until the child exits.
|
|
99
|
+
- Asyncio-native surface (tokio ↔ asyncio bridge). Cancelling an awaited run —
|
|
100
|
+
directly, or via `asyncio.wait_for` / `asyncio.timeout` — tears down the whole
|
|
101
|
+
process tree and raises `asyncio.CancelledError`.
|
|
102
|
+
- `Command`: `aoutput()`, `aoutput_bytes()`, `arun()`, `aexit_code()`,
|
|
103
|
+
`aprobe()`, and `astart()` (returns a `RunningProcess` for
|
|
104
|
+
streaming/interactive I/O).
|
|
105
|
+
- `RunningProcess`: `async for line in proc.stdout_lines()`, `output_events()`
|
|
106
|
+
(stdout+stderr as `OutputEvent`s), interactive `take_stdin()` →
|
|
107
|
+
`ProcessStdin` (`write`/`write_line`/`flush`/`close`), and `await`able
|
|
108
|
+
`wait()` → `Outcome`, `finish()` → `Finished`, `output()` → `ProcessResult`,
|
|
109
|
+
plus `kill()` / `shutdown(grace_seconds)`. It is also a context manager
|
|
110
|
+
(`with` / `async with`): exiting the block tears the process down
|
|
111
|
+
deterministically — a hard kill of the whole private tree for a standalone
|
|
112
|
+
`start()`/`astart()` handle — without relying on Python's GC.
|
|
113
|
+
- `ProcessGroup`: `async with`, `astart()`, `ashutdown()`.
|
|
114
|
+
- `Command` stdin configuration: `stdin_bytes()` / `stdin_text()` (feed input
|
|
115
|
+
upfront) and `keep_stdin_open()` (write interactively after start).
|
|
116
|
+
- New result types: `Outcome`, `Finished`, `OutputEvent`.
|
|
117
|
+
- Higher-level features:
|
|
118
|
+
- **Resource limits** on `ProcessGroup`: keyword-only `max_memory`,
|
|
119
|
+
`max_processes`, `cpu_quota`, `shutdown_grace`, `escalate_to_kill`
|
|
120
|
+
(enforced via the Windows Job Object or a Linux cgroup-v2 *root*).
|
|
121
|
+
- **Signals & observability** on `ProcessGroup`: `signal("term"|…)`,
|
|
122
|
+
`suspend()`, `resume()`, `kill_all()`, and `stats()` →
|
|
123
|
+
`ProcessGroupStats`.
|
|
124
|
+
- **Pipelines**: `Command | Command` (or `.pipe()`) → `Pipeline`, with the
|
|
125
|
+
sync/async run verbs (incl. `output_bytes()` / `aoutput_bytes()` for a binary
|
|
126
|
+
tail) and `timeout()`.
|
|
127
|
+
- **Supervision**: `Supervisor(cmd, restart=…, max_restarts=…, backoff_initial=…,
|
|
128
|
+
backoff_factor=…, max_backoff=…, jitter=…, stop_when=…, storm_pause=…,
|
|
129
|
+
failure_threshold=…, failure_decay=…)` with `run()` / `arun()` →
|
|
130
|
+
`SupervisionOutcome`. Setting `storm_pause` enables the failure-storm guard
|
|
131
|
+
(crash-loop circuit-breaker), reported via `SupervisionOutcome.storm_pauses`.
|
|
132
|
+
- **Readiness probes**: `await wait_for_port(host, port, *, timeout)`,
|
|
133
|
+
`await wait_for_line(lines, predicate, *, timeout)`, and
|
|
134
|
+
`await wait_for(predicate, *, timeout)` (poll any sync-or-async condition).
|
|
135
|
+
- New types/exception: `Pipeline`, `ProcessGroupStats`, `Supervisor`,
|
|
136
|
+
`SupervisionOutcome`, `ResourceLimit`.
|
|
137
|
+
- Testing seam: a `Runner` (real) and a `ScriptedRunner` (test double) with a
|
|
138
|
+
uniform sync + async (`a`-prefixed) `output`/`run`/`exit_code`/`probe`/`start`
|
|
139
|
+
interface, plus `Reply`
|
|
140
|
+
(`ok`/`fail`/`timeout`/`signalled`/`lines`/`pending`). Inject a `Runner` in
|
|
141
|
+
production and a `ScriptedRunner` in tests — no real processes spawned; the
|
|
142
|
+
results returned are genuine `ProcessResult` / `RunningProcess` objects. The
|
|
143
|
+
injected runner is typed by the `ProcessRunner` `typing.Protocol`, which
|
|
144
|
+
`Runner` / `ScriptedRunner` / `RecordReplayRunner` / `RecordingRunner` all
|
|
145
|
+
satisfy structurally. The test doubles (`ScriptedRunner`, `RecordReplayRunner`,
|
|
146
|
+
`RecordingRunner`) plus `Reply` and `Invocation` live in the **`processkit.testing`**
|
|
147
|
+
submodule; `Runner` and `ProcessRunner` are top-level (production).
|
|
148
|
+
- A full [documentation guide set](docs/README.md): a task-oriented
|
|
149
|
+
[cookbook](docs/cookbook.md) plus deep guides for
|
|
150
|
+
[running commands](docs/commands.md), [process groups](docs/process-groups.md),
|
|
151
|
+
[streaming & interactive I/O](docs/streaming.md), [pipelines](docs/pipelines.md),
|
|
152
|
+
[timeouts & cancellation](docs/timeouts-and-cancellation.md),
|
|
153
|
+
[supervision](docs/supervision.md), and [testing](docs/testing.md), tied
|
|
154
|
+
together by a progressively-disclosed README with a cover illustration.
|
|
155
|
+
- Type stubs (`_processkit.pyi`) for the compiled extension.
|
|
156
|
+
- A [platform support & caveats](docs/platforms.md) matrix documenting per-OS
|
|
157
|
+
teardown, resource-limit, signal, and stats behaviour.
|
|
158
|
+
- **Stability commitment:** as of 1.0 the public API follows SemVer — breaking
|
|
159
|
+
changes land only in a new major version.
|
|
160
|
+
- **Free-threaded CPython (PEP 703):** the extension declares `gil_used = false`,
|
|
161
|
+
so importing it on a free-threaded build (CPython 3.14t) does **not** re-enable
|
|
162
|
+
the GIL. Shipped as a version-specific free-threaded wheel alongside the
|
|
163
|
+
abi3 (GIL) wheel, and the full test suite runs on the free-threaded interpreter
|
|
164
|
+
in CI. Also adds CPython **3.14** to the supported set (the abi3 wheel already
|
|
165
|
+
runs there).
|
|
166
|
+
- **musllinux (Alpine/musl) wheels** for x86_64 and aarch64, alongside the
|
|
167
|
+
existing manylinux (glibc) wheels — so `pip install` gets a binary wheel on
|
|
168
|
+
Alpine-based images instead of building from the sdist. Both the abi3 GIL wheel
|
|
169
|
+
and the free-threaded cp314t wheel ship per libc. CI builds and smoke-tests the
|
|
170
|
+
x86_64 musllinux wheels on every push (aarch64 builds natively at release).
|
|
171
|
+
- Packaging metadata for the PyPI page: Trove classifiers (CPython 3.10–3.14, the
|
|
172
|
+
supported operating systems, topics) and project URLs (Documentation, Issues).
|
|
173
|
+
- Runnable [`examples/`](examples/) — self-contained, cross-platform programs, one
|
|
174
|
+
per target niche (whole-tree no-orphan teardown, a readiness-gated server,
|
|
175
|
+
supervision-until-healthy, a resource-limited sandbox). Each is exercised in CI.
|
|
176
|
+
- Docs: a **"Coming from subprocess"** guide that maps `subprocess` /
|
|
177
|
+
`asyncio.subprocess` patterns onto their processkit equivalents (verbs, flags,
|
|
178
|
+
pipelines, the exception mapping) and shows the whole-tree containment the stdlib
|
|
179
|
+
can't express.
|
|
180
|
+
|
|
181
|
+
### Changed
|
|
182
|
+
- Renamed `Command.ok_codes()` → **`success_codes()`** (clearer that it is the
|
|
183
|
+
whole success set, not an addition), and an empty sequence now raises
|
|
184
|
+
`ValueError` instead of being silently ignored.
|
|
185
|
+
- Renamed `RunProfile.exit_code` → **`code`**, matching the exit-code field on
|
|
186
|
+
every other result type (`ProcessResult`, `Outcome`, …).
|
|
187
|
+
- `Command.encoding()` / `stdout_encoding` / `stderr_encoding` now also accept
|
|
188
|
+
common **Python codec aliases** (`latin_1`, `utf_8`, `euc_jp`, …) in addition to
|
|
189
|
+
WHATWG labels, normalized to the WHATWG form; an unmappable label raises
|
|
190
|
+
`ValueError` naming the WHATWG equivalent. (WHATWG `iso-8859-1` / Python
|
|
191
|
+
`latin_1` decode as windows-1252.)
|
|
192
|
+
- `Command.arg()` / `args()` and the `Command(...)` constructor's args accept any
|
|
193
|
+
`os.PathLike[str]` (e.g. `pathlib.Path`), not only `str`, so a `Path` argument
|
|
194
|
+
needs no `str()`. (`bytes` paths are not accepted; `StrPath` was narrowed to
|
|
195
|
+
`str | os.PathLike[str]` to match.)
|
|
196
|
+
- Closed-set string parameters and return values are typed as `Literal` in the
|
|
197
|
+
stubs (signal names, `restart`, `mechanism`, `SupervisionOutcome.stopped`,
|
|
198
|
+
`OutputEvent.stream`) for editor autocomplete and `mypy` typo-catching.
|
|
199
|
+
- Exported the `StrPath` (`str | os.PathLike[str]`) and `SignalName` (the signal-name
|
|
200
|
+
`Literal`) type aliases from the package, so your own wrappers can annotate against
|
|
201
|
+
the same types the API accepts.
|
|
202
|
+
- Renamed `ProcessGroup(memory_max=…)` → **`max_memory`**, so every ceiling on the
|
|
203
|
+
surface follows the `max_*` convention (`max_processes`, `output_limit(max_bytes=…,
|
|
204
|
+
max_lines=…)`, `Supervisor(max_restarts=…, max_backoff=…)`). The crate builder
|
|
205
|
+
remains `memory_max()`.
|
|
206
|
+
- Renamed `RunProfile.avg_cpu` → **`avg_cpu_cores`** (self-documenting: the value is
|
|
207
|
+
CPU-cores, e.g. `1.7` ≈ 1.7 cores busy).
|
|
208
|
+
- Renamed `RunningProcess.start_kill()` → **`kill()`**, matching
|
|
209
|
+
`subprocess.Popen.kill()` (fire-and-forget; does not wait for exit).
|
|
210
|
+
- Renamed `ProcessGroup.terminate_all()` → **`kill_all()`** and the
|
|
211
|
+
`ProcessGroup(shutdown_timeout=…)` ceiling → **`shutdown_grace`**, so the group's
|
|
212
|
+
teardown surface reads as what it does — a hard kill of the whole tree, after an
|
|
213
|
+
optional grace period — and lines up with `RunningProcess.kill()` and
|
|
214
|
+
`Command.timeout_grace()`. The crate keeps `terminate_all()` / `shutdown_timeout()`.
|
|
215
|
+
- Renamed the `OutputTooLarge` overflow fields `line_limit` / `byte_limit` →
|
|
216
|
+
**`max_lines`** / **`max_bytes`**, so the caps reported on overflow match the
|
|
217
|
+
`output_limit(max_bytes=…, max_lines=…)` kwargs that set them.
|
|
218
|
+
- Moved the runner test doubles — `ScriptedRunner`, `RecordReplayRunner`,
|
|
219
|
+
`RecordingRunner`, the `Reply` builder, and the `Invocation` record — into a new
|
|
220
|
+
**`processkit.testing`** submodule (mirroring the crate's `processkit::testing`
|
|
221
|
+
split), so the top-level `processkit` namespace is the production surface and the
|
|
222
|
+
test scaffolding is one explicit import away (`from processkit.testing import
|
|
223
|
+
ScriptedRunner`). `Runner` and the `ProcessRunner` protocol stay top-level.
|
|
224
|
+
- `ProcessResult.combined` is now a **property** (was `combined()`), matching the
|
|
225
|
+
other read accessors (`stdout`, `code`, …).
|
|
226
|
+
- Renamed `Outcome.is_success` / `Finished.is_success` → **`exited_zero`**. These
|
|
227
|
+
test literal "exit code 0" and — unlike `ProcessResult.is_success` — carry no
|
|
228
|
+
`success_codes` context, so the new name no longer implies the command's own
|
|
229
|
+
success verdict. Use `ProcessResult.is_success`, or test `code` against your set.
|
|
230
|
+
- `RunningProcess.take_stdin()` now **raises** `ProcessError` (instead of returning
|
|
231
|
+
`None`) when stdin was not kept open or was already taken — so a missing
|
|
232
|
+
`keep_stdin_open()` fails at the call, not later with an `AttributeError`. Its
|
|
233
|
+
return type is now `ProcessStdin` (no longer `... | None`).
|
|
234
|
+
- The readiness helpers `wait_for()` / `wait_for_port()` / `wait_for_line()` now
|
|
235
|
+
take `timeout` as a **keyword-only** argument, for uniformity.
|
|
236
|
+
|
|
237
|
+
### Removed
|
|
238
|
+
- `Cancelled` exception. It was never raised from the Python surface (the binding
|
|
239
|
+
exposes no cancellation token; cancelling an awaited run surfaces as
|
|
240
|
+
`asyncio.CancelledError`), so it was pure catch-list clutter. Re-addable
|
|
241
|
+
(additive) if a token-style cancellation API is ever exposed.
|
|
242
|
+
- `CliClient.run_unit()` / `arun_unit()`. The success-only `-> None` verb existed
|
|
243
|
+
nowhere else on the surface; use `run()` / `arun()` and ignore the returned
|
|
244
|
+
stdout for the same "run, raise on failure" behavior.
|
|
245
|
+
- `ResourceLimit.message`. It duplicated `str(exc)` — idiomatic Python 3 exceptions
|
|
246
|
+
carry no separate `.message` attribute. Read the reason via `str(exc)`.
|
|
247
|
+
|
|
248
|
+
### Fixed
|
|
249
|
+
- A synchronous verb called from inside a `Supervisor` `stop_when` predicate no
|
|
250
|
+
longer re-enters the tokio runtime and panics (the panic was previously
|
|
251
|
+
swallowed, so the predicate silently never fired); it now raises a clear
|
|
252
|
+
`ProcessError`. Documented that the predicate must read the result handed to it
|
|
253
|
+
rather than run new verbs.
|
|
254
|
+
- `Supervisor(backoff_factor=…)` is now applied (and validated) independently of
|
|
255
|
+
`backoff_initial` — previously the factor was silently dropped unless
|
|
256
|
+
`backoff_initial` was also passed.
|
|
257
|
+
- A `RecordReplayRunner.replay()` cassette miss now carries the `.program` field,
|
|
258
|
+
matching every other program-bearing `ProcessError`.
|
|
259
|
+
- `wait_for_port()` no longer leaks the probe socket if the awaiting task is
|
|
260
|
+
cancelled just after the connection is accepted.
|
|
261
|
+
- `wait_for()` now bounds its predicate by `timeout` — an async predicate that
|
|
262
|
+
hangs no longer ignores the deadline — while propagating the predicate's own
|
|
263
|
+
exception unchanged and cancelling the in-flight predicate (rather than orphaning
|
|
264
|
+
it) when the awaiting task is cancelled.
|
|
265
|
+
|
|
266
|
+
### Security
|
|
267
|
+
- `repr(Command(...))` no longer renders argv (or env *values*): it now uses the
|
|
268
|
+
crate's redacted form — program, argument *count*, and env *names* only. A repr
|
|
269
|
+
is emitted everywhere (logging `%r`, f-strings, tracebacks, test diffs), so this
|
|
270
|
+
prevents a secret passed as an argument from leaking through any of them. (The
|
|
271
|
+
Python surface exposes no way to recover the full command line; argv remains
|
|
272
|
+
visible to the OS via `ps` / `/proc` while the child runs.)
|
|
273
|
+
- Documentation hardening: the sandbox/privilege-drop guidance now sets all of
|
|
274
|
+
`gid` / `groups` / `uid` (dropping `uid` alone leaves the child holding the
|
|
275
|
+
parent's supplementary groups — a sandbox-escape footgun); documents that
|
|
276
|
+
record/replay cassettes are written owner-only (`0600`, no symlink follow) on
|
|
277
|
+
Unix; and warns that exception `stdout`/`stderr` still carry raw values — pass
|
|
278
|
+
secrets via `env(...)`, not flags.
|
|
279
|
+
|
|
280
|
+
### Notes
|
|
281
|
+
|
|
282
|
+
- This is the **1.0** release: the public API is frozen.
|
|
283
|
+
- Distributed as abi3 wheels for CPython 3.10+ (standard/GIL builds), **plus a
|
|
284
|
+
version-specific free-threaded wheel** for CPython 3.14t (PEP 703).
|
|
285
|
+
- The `RecordReplayRunner` test double enables the crate's `record` feature,
|
|
286
|
+
which pulls `serde` / `serde_json` into the compiled wheel.
|
|
287
|
+
- `enable_logging()` enables the crate's `tracing` feature; the bridge pulls
|
|
288
|
+
`tracing` / `tracing-subscriber` (registry only) into the compiled wheel.
|
|
289
|
+
|
|
290
|
+
[Unreleased]: https://github.com/ZelAnton/processkit-py/compare/v1.0.0...HEAD
|
|
291
|
+
[1.0.0]: https://github.com/ZelAnton/processkit-py/releases/tag/v1.0.0
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Contributing to processkit
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in improving **processkit**.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Python 3.12 (uv provisions the exact interpreter pinned in `.python-version`).
|
|
8
|
+
- [uv](https://docs.astral.sh/uv/) on your PATH — run `scripts/check-env.sh`
|
|
9
|
+
(or `scripts/check-env.ps1`) to confirm.
|
|
10
|
+
- A Rust toolchain — install via [rustup](https://rustup.rs/).
|
|
11
|
+
|
|
12
|
+
## Build and test
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
uv run maturin develop # build the Rust extension and install it in-place
|
|
16
|
+
uv run pytest # run the tests (requires maturin develop first)
|
|
17
|
+
uv run ruff format --check . # formatting must be clean
|
|
18
|
+
uv run ruff check . # lint
|
|
19
|
+
uv run mypy # type-check (strict)
|
|
20
|
+
uv run maturin build --release --out dist # build a release abi3 wheel
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`ruff check`, `mypy --strict`, and `pytest` (with warnings promoted to errors)
|
|
24
|
+
are the gates CI enforces, so run them locally before opening a pull request.
|
|
25
|
+
CI additionally runs `cargo fmt --check` and `cargo clippy -- -D warnings`.
|
|
26
|
+
|
|
27
|
+
## Pre-commit (optional but recommended)
|
|
28
|
+
|
|
29
|
+
A [pre-commit](https://pre-commit.com/) config mirrors the formatting/lint gates
|
|
30
|
+
so they run automatically on `git commit`:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
uv run pre-commit install # set up the git hook (once)
|
|
34
|
+
uv run pre-commit run --all-files # run against the whole tree
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
It runs ruff (lint + format) and `cargo fmt`; `cargo clippy` and the test suite
|
|
38
|
+
stay in CI (too slow for a commit hook). Keep hook versions current with
|
|
39
|
+
`uv run pre-commit autoupdate`.
|
|
40
|
+
|
|
41
|
+
## Testing on Linux with Docker
|
|
42
|
+
|
|
43
|
+
Some behaviour only runs on Linux/macOS — the cgroup/process-group teardown,
|
|
44
|
+
async cancellation, and the `Ctrl+C` interrupt test are skipped on Windows. To
|
|
45
|
+
exercise them from a Windows (or any) host, run the suite in a container:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
docker compose run --build --rm test
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This builds the PyO3 extension with a real Rust toolchain + uv and runs `pytest`
|
|
52
|
+
on Linux. The container is `privileged` so the crate selects the `cgroup_v2`
|
|
53
|
+
mechanism — the same path CI's Linux runner uses; drop `privileged` in
|
|
54
|
+
[`compose.yaml`](compose.yaml) to test the `process_group` fallback instead.
|
|
55
|
+
Append a command to scope the run:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
docker compose run --build --rm test uv run pytest -q tests/test_async.py
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
It needs a Docker-compatible engine (Docker Desktop, Rancher Desktop, …) and
|
|
62
|
+
writes nothing to your working tree. It complements — does not replace — the
|
|
63
|
+
native `uv run pytest`, which is faster for day-to-day work.
|
|
64
|
+
|
|
65
|
+
## Conventions
|
|
66
|
+
|
|
67
|
+
- **Formatting and linting** are governed by [`ruff`](https://docs.astral.sh/ruff/)
|
|
68
|
+
(config in [`pyproject.toml`](pyproject.toml)). Run `uv run ruff format .` to
|
|
69
|
+
apply formatting; don't reformat code you are not changing.
|
|
70
|
+
- **Dependencies** are declared in `pyproject.toml` and pinned in `uv.lock`
|
|
71
|
+
(commit the lockfile). Add them with `uv add`, not by hand.
|
|
72
|
+
- The authoritative bar is simply what CI enforces — `ruff`, `mypy --strict`, and
|
|
73
|
+
warning-free `pytest`, plus `cargo fmt` / `clippy` on the Rust side — all
|
|
74
|
+
configured in [`pyproject.toml`](pyproject.toml); run the
|
|
75
|
+
[gates above](#build-and-test) locally before opening a pull request.
|
|
76
|
+
|
|
77
|
+
## Changelog
|
|
78
|
+
|
|
79
|
+
Every user-visible change ships its [`CHANGELOG.md`](CHANGELOG.md) entry in the
|
|
80
|
+
same change set, under `## [Unreleased]`. Write the bullet for a consumer of the
|
|
81
|
+
library, not the implementer. Pure internal refactors are exempt.
|
|
82
|
+
|
|
83
|
+
## Pull requests
|
|
84
|
+
|
|
85
|
+
- Keep changes focused; unrelated cleanups belong in their own PR.
|
|
86
|
+
- Ensure CI (lint, type-check, and tests on Linux, Windows, macOS) passes.
|
|
87
|
+
- Fill in the pull-request checklist.
|