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.
Files changed (35) hide show
  1. {spawnllm-0.17.2 → spawnllm-0.17.3}/PKG-INFO +1 -1
  2. {spawnllm-0.17.2 → spawnllm-0.17.3}/pyproject.toml +1 -1
  3. {spawnllm-0.17.2 → spawnllm-0.17.3}/pyproject.toml.orig +1 -1
  4. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/proc.py +61 -9
  5. {spawnllm-0.17.2 → spawnllm-0.17.3}/LICENSE +0 -0
  6. {spawnllm-0.17.2 → spawnllm-0.17.3}/README.md +0 -0
  7. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/__init__.py +0 -0
  8. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/__main__.py +0 -0
  9. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/_core/__init__.py +0 -0
  10. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/_core/spawnllm_core.wasm +0 -0
  11. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/__init__.py +0 -0
  12. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/apple.py +0 -0
  13. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/base.py +0 -0
  14. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/claude.py +0 -0
  15. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/claude_sdk.py +0 -0
  16. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/codex.py +0 -0
  17. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/gemini.py +0 -0
  18. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/mlx.py +0 -0
  19. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/openai_endpoint.py +0 -0
  20. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/backends/registry.py +0 -0
  21. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/call.py +0 -0
  22. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/cli.py +0 -0
  23. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/decide.py +0 -0
  24. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/extract.py +0 -0
  25. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/mlx/__init__.py +0 -0
  26. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/mlx/codec.py +0 -0
  27. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/mlx/engine.py +0 -0
  28. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/mlx/fuse.py +0 -0
  29. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/mlx/patches.py +0 -0
  30. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/py.typed +0 -0
  31. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/response.py +0 -0
  32. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/run.py +0 -0
  33. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/spec.py +0 -0
  34. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/structured.py +0 -0
  35. {spawnllm-0.17.2 → spawnllm-0.17.3}/spawnllm/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spawnllm
3
- Version: 0.17.2
3
+ Version: 0.17.3
4
4
  Summary: Delete your subprocess wrappers around claude, codex, and gemini.
5
5
  Keywords:
6
6
  Author: Yasyf Mohamedali
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spawnllm"
3
- version = "0.17.2"
3
+ version = "0.17.3"
4
4
  description = "Delete your subprocess wrappers around claude, codex, and gemini."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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.2"
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
- result = subprocess.run(
62
- argv,
63
- input=input,
64
- capture_output=True,
65
- text=True,
66
- timeout=timeout,
67
- env=env,
68
- cwd=cwd,
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