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.
Files changed (81) hide show
  1. processkit_py-1.0.0/.editorconfig +49 -0
  2. processkit_py-1.0.0/.gitattributes +11 -0
  3. processkit_py-1.0.0/.gitignore +89 -0
  4. processkit_py-1.0.0/.pre-commit-config.yaml +36 -0
  5. processkit_py-1.0.0/.python-version +1 -0
  6. processkit_py-1.0.0/.yamllint.yml +34 -0
  7. processkit_py-1.0.0/CHANGELOG.md +291 -0
  8. processkit_py-1.0.0/CONTRIBUTING.md +87 -0
  9. processkit_py-1.0.0/Cargo.lock +525 -0
  10. processkit_py-1.0.0/Cargo.toml +70 -0
  11. processkit_py-1.0.0/LICENSE +21 -0
  12. processkit_py-1.0.0/PKG-INFO +490 -0
  13. processkit_py-1.0.0/README.md +456 -0
  14. processkit_py-1.0.0/RELEASING.md +96 -0
  15. processkit_py-1.0.0/ROADMAP.md +236 -0
  16. processkit_py-1.0.0/SECURITY.md +41 -0
  17. processkit_py-1.0.0/cliff.toml +45 -0
  18. processkit_py-1.0.0/docs/README.md +112 -0
  19. processkit_py-1.0.0/docs/commands.md +366 -0
  20. processkit_py-1.0.0/docs/cookbook.md +479 -0
  21. processkit_py-1.0.0/docs/migrating.md +185 -0
  22. processkit_py-1.0.0/docs/pipelines.md +179 -0
  23. processkit_py-1.0.0/docs/platforms.md +81 -0
  24. processkit_py-1.0.0/docs/process-groups.md +282 -0
  25. processkit_py-1.0.0/docs/streaming.md +323 -0
  26. processkit_py-1.0.0/docs/supervision.md +192 -0
  27. processkit_py-1.0.0/docs/testing.md +278 -0
  28. processkit_py-1.0.0/docs/timeouts-and-cancellation.md +226 -0
  29. processkit_py-1.0.0/examples/01_no_orphan_guarantee.py +52 -0
  30. processkit_py-1.0.0/examples/02_wait_for_server.py +98 -0
  31. processkit_py-1.0.0/examples/03_supervise_until_healthy.py +59 -0
  32. processkit_py-1.0.0/examples/04_sandbox_resource_limits.py +82 -0
  33. processkit_py-1.0.0/examples/README.md +24 -0
  34. processkit_py-1.0.0/mkdocs.yml +66 -0
  35. processkit_py-1.0.0/pyproject.toml +138 -0
  36. processkit_py-1.0.0/rust-toolchain.toml +5 -0
  37. processkit_py-1.0.0/scripts/check-env.ps1 +64 -0
  38. processkit_py-1.0.0/scripts/check-env.sh +55 -0
  39. processkit_py-1.0.0/scripts/smoke.py +23 -0
  40. processkit_py-1.0.0/src/batch.rs +133 -0
  41. processkit_py-1.0.0/src/cli.rs +121 -0
  42. processkit_py-1.0.0/src/command.rs +492 -0
  43. processkit_py-1.0.0/src/convert.rs +139 -0
  44. processkit_py-1.0.0/src/errors.rs +202 -0
  45. processkit_py-1.0.0/src/group.rs +269 -0
  46. processkit_py-1.0.0/src/lib.rs +104 -0
  47. processkit_py-1.0.0/src/logging.rs +110 -0
  48. processkit_py-1.0.0/src/processkit/__init__.py +111 -0
  49. processkit_py-1.0.0/src/processkit/_aio.py +175 -0
  50. processkit_py-1.0.0/src/processkit/_processkit.pyi +658 -0
  51. processkit_py-1.0.0/src/processkit/_runner.py +46 -0
  52. processkit_py-1.0.0/src/processkit/_types.py +17 -0
  53. processkit_py-1.0.0/src/processkit/py.typed +0 -0
  54. processkit_py-1.0.0/src/processkit/testing.py +30 -0
  55. processkit_py-1.0.0/src/result.rs +407 -0
  56. processkit_py-1.0.0/src/runner.rs +535 -0
  57. processkit_py-1.0.0/src/running.rs +383 -0
  58. processkit_py-1.0.0/src/runtime.rs +87 -0
  59. processkit_py-1.0.0/src/supervisor.rs +231 -0
  60. processkit_py-1.0.0/stubtest-allowlist.txt +38 -0
  61. processkit_py-1.0.0/tests/__init__.py +0 -0
  62. processkit_py-1.0.0/tests/_liveness.py +94 -0
  63. processkit_py-1.0.0/tests/_programs.py +28 -0
  64. processkit_py-1.0.0/tests/_typing_pins.py +140 -0
  65. processkit_py-1.0.0/tests/test_api_surface.py +277 -0
  66. processkit_py-1.0.0/tests/test_async.py +138 -0
  67. processkit_py-1.0.0/tests/test_batch.py +72 -0
  68. processkit_py-1.0.0/tests/test_cli_client.py +69 -0
  69. processkit_py-1.0.0/tests/test_command.py +427 -0
  70. processkit_py-1.0.0/tests/test_examples.py +45 -0
  71. processkit_py-1.0.0/tests/test_exceptions.py +147 -0
  72. processkit_py-1.0.0/tests/test_hardening.py +57 -0
  73. processkit_py-1.0.0/tests/test_import.py +10 -0
  74. processkit_py-1.0.0/tests/test_logging.py +41 -0
  75. processkit_py-1.0.0/tests/test_pipelines.py +116 -0
  76. processkit_py-1.0.0/tests/test_process_group.py +221 -0
  77. processkit_py-1.0.0/tests/test_readiness.py +316 -0
  78. processkit_py-1.0.0/tests/test_runner_seam.py +377 -0
  79. processkit_py-1.0.0/tests/test_streaming.py +353 -0
  80. processkit_py-1.0.0/tests/test_supervisor.py +152 -0
  81. 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.