spawnllm 0.17.2__tar.gz → 0.17.3__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.
- {spawnllm-0.17.2 → spawnllm-0.17.3}/PKG-INFO +1 -1
- {spawnllm-0.17.2 → spawnllm-0.17.3}/pyproject.toml +1 -1
- {spawnllm-0.17.2 → spawnllm-0.17.3}/pyproject.toml.orig +1 -1
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/proc.py +61 -9
- {spawnllm-0.17.2 → spawnllm-0.17.3}/LICENSE +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/README.md +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/__init__.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/__main__.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/_core/__init__.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/_core/spawnllm_core.wasm +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/__init__.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/apple.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/base.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/claude.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/claude_sdk.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/codex.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/gemini.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/mlx.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/openai_endpoint.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/registry.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/call.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/cli.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/decide.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/extract.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/mlx/__init__.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/mlx/codec.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/mlx/engine.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/mlx/fuse.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/mlx/patches.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/py.typed +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/response.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/run.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/spec.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/structured.py +0 -0
- {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/types.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "spawnllm"
|
|
3
3
|
# Inert sentinel: the real version is set from the release tag (uv version --frozen).
|
|
4
|
-
version = "0.17.
|
|
4
|
+
version = "0.17.3"
|
|
5
5
|
description = "Delete your subprocess wrappers around claude, codex, and gemini."
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "MIT"
|
|
@@ -7,6 +7,7 @@ import contextlib
|
|
|
7
7
|
import os
|
|
8
8
|
import subprocess
|
|
9
9
|
import tempfile
|
|
10
|
+
import time
|
|
10
11
|
from collections.abc import Awaitable, Callable, Iterator, Sequence
|
|
11
12
|
from dataclasses import dataclass
|
|
12
13
|
from pathlib import Path
|
|
@@ -16,6 +17,7 @@ __all__ = ["RunResult", "acapture_cli", "arun_cli", "capture_cli", "collect_proc
|
|
|
16
17
|
# Drain stderr in fixed chunks: `StreamReader.readline` (what `async for` uses) caps a
|
|
17
18
|
# line at 64 KiB and raises `LimitOverrunError` on a longer newline-free blob.
|
|
18
19
|
_STDERR_CHUNK = 1 << 16
|
|
20
|
+
CHECKPOINT_INTERVAL = 0.05
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
@dataclass(frozen=True, slots=True)
|
|
@@ -40,6 +42,7 @@ def run_cli(
|
|
|
40
42
|
timeout: int = 30,
|
|
41
43
|
env: dict[str, str] | None = None,
|
|
42
44
|
cwd: str | None = None,
|
|
45
|
+
checkpoint: Callable[[], None] | None = None,
|
|
43
46
|
) -> str:
|
|
44
47
|
"""Run a CLI command to completion and return its stdout.
|
|
45
48
|
|
|
@@ -49,6 +52,8 @@ def run_cli(
|
|
|
49
52
|
timeout: Seconds to wait before the process is killed.
|
|
50
53
|
env: Environment for the process; `None` inherits the current environment.
|
|
51
54
|
cwd: Working directory for the process.
|
|
55
|
+
checkpoint: POSIX-only check before spawning, while waiting, and before returning.
|
|
56
|
+
An exception stops and reaps the direct child, then propagates unchanged.
|
|
52
57
|
|
|
53
58
|
Returns:
|
|
54
59
|
The decoded stdout.
|
|
@@ -57,16 +62,20 @@ def run_cli(
|
|
|
57
62
|
subprocess.CalledProcessError: On a nonzero exit code, with the argv,
|
|
58
63
|
exit code, and stdout/stderr tails attached as notes.
|
|
59
64
|
subprocess.TimeoutExpired: When the process outlives `timeout`.
|
|
65
|
+
NotImplementedError: When a checkpoint is requested on a non-POSIX platform.
|
|
60
66
|
"""
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
if checkpoint is None:
|
|
68
|
+
result = subprocess.run(
|
|
69
|
+
argv,
|
|
70
|
+
input=input,
|
|
71
|
+
capture_output=True,
|
|
72
|
+
text=True,
|
|
73
|
+
timeout=timeout,
|
|
74
|
+
env=env,
|
|
75
|
+
cwd=cwd,
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
result = cancellable_cli(argv, input=input, timeout=timeout, env=env, cwd=cwd, checkpoint=checkpoint)
|
|
70
79
|
if result.returncode != 0:
|
|
71
80
|
err = subprocess.CalledProcessError(result.returncode, argv, output=result.stdout, stderr=result.stderr)
|
|
72
81
|
err.add_note(f"argv: {argv}")
|
|
@@ -77,6 +86,49 @@ def run_cli(
|
|
|
77
86
|
return result.stdout
|
|
78
87
|
|
|
79
88
|
|
|
89
|
+
def cancellable_cli(
|
|
90
|
+
argv: list[str],
|
|
91
|
+
*,
|
|
92
|
+
input: str | None,
|
|
93
|
+
timeout: int,
|
|
94
|
+
env: dict[str, str] | None,
|
|
95
|
+
cwd: str | None,
|
|
96
|
+
checkpoint: Callable[[], None],
|
|
97
|
+
) -> subprocess.CompletedProcess[str]:
|
|
98
|
+
if os.name != "posix":
|
|
99
|
+
raise NotImplementedError("CLI cancellation checkpoints require POSIX")
|
|
100
|
+
checkpoint()
|
|
101
|
+
with subprocess.Popen(
|
|
102
|
+
argv,
|
|
103
|
+
stdin=subprocess.PIPE if input is not None else None,
|
|
104
|
+
stdout=subprocess.PIPE,
|
|
105
|
+
stderr=subprocess.PIPE,
|
|
106
|
+
text=True,
|
|
107
|
+
env=env,
|
|
108
|
+
cwd=cwd,
|
|
109
|
+
) as child:
|
|
110
|
+
deadline = time.monotonic() + timeout
|
|
111
|
+
partial = subprocess.TimeoutExpired(argv, timeout)
|
|
112
|
+
payload = input
|
|
113
|
+
try:
|
|
114
|
+
while True:
|
|
115
|
+
checkpoint()
|
|
116
|
+
if (remaining := deadline - time.monotonic()) <= 0:
|
|
117
|
+
raise subprocess.TimeoutExpired(argv, timeout, output=partial.output, stderr=partial.stderr)
|
|
118
|
+
try:
|
|
119
|
+
stdout, stderr = child.communicate(input=payload, timeout=min(CHECKPOINT_INTERVAL, remaining))
|
|
120
|
+
except subprocess.TimeoutExpired as expired:
|
|
121
|
+
partial = expired
|
|
122
|
+
else:
|
|
123
|
+
checkpoint()
|
|
124
|
+
return subprocess.CompletedProcess(argv, child.wait(), stdout, stderr)
|
|
125
|
+
payload = None
|
|
126
|
+
except BaseException:
|
|
127
|
+
child.kill()
|
|
128
|
+
child.wait()
|
|
129
|
+
raise
|
|
130
|
+
|
|
131
|
+
|
|
80
132
|
def capture_cli(
|
|
81
133
|
argv: list[str],
|
|
82
134
|
*,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|