codebuddy-agent-sdk 0.1.15__py3-none-any.whl
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.
Potentially problematic release.
This version of codebuddy-agent-sdk might be problematic. Click here for more details.
- codebuddy_agent_sdk/__init__.py +97 -0
- codebuddy_agent_sdk/_binary.py +148 -0
- codebuddy_agent_sdk/_errors.py +39 -0
- codebuddy_agent_sdk/_message_parser.py +112 -0
- codebuddy_agent_sdk/_version.py +3 -0
- codebuddy_agent_sdk/bin/codebuddy.exe +0 -0
- codebuddy_agent_sdk/client.py +298 -0
- codebuddy_agent_sdk/py.typed +0 -0
- codebuddy_agent_sdk/query.py +316 -0
- codebuddy_agent_sdk/transport/__init__.py +6 -0
- codebuddy_agent_sdk/transport/base.py +24 -0
- codebuddy_agent_sdk/transport/subprocess.py +167 -0
- codebuddy_agent_sdk/types.py +303 -0
- codebuddy_agent_sdk-0.1.15.dist-info/METADATA +89 -0
- codebuddy_agent_sdk-0.1.15.dist-info/RECORD +16 -0
- codebuddy_agent_sdk-0.1.15.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Query function for one-shot interactions with CodeBuddy."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import AsyncIterable, AsyncIterator
|
|
6
|
+
from dataclasses import asdict
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ._message_parser import parse_message
|
|
10
|
+
from .transport import SubprocessTransport, Transport
|
|
11
|
+
from .types import (
|
|
12
|
+
AppendSystemPrompt,
|
|
13
|
+
CanUseToolOptions,
|
|
14
|
+
CodeBuddyAgentOptions,
|
|
15
|
+
HookMatcher,
|
|
16
|
+
Message,
|
|
17
|
+
ResultMessage,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def query(
|
|
22
|
+
*,
|
|
23
|
+
prompt: str | AsyncIterable[dict[str, Any]],
|
|
24
|
+
options: CodeBuddyAgentOptions | None = None,
|
|
25
|
+
transport: Transport | None = None,
|
|
26
|
+
) -> AsyncIterator[Message]:
|
|
27
|
+
"""
|
|
28
|
+
Query CodeBuddy for one-shot or unidirectional streaming interactions.
|
|
29
|
+
|
|
30
|
+
This function is ideal for simple, stateless queries where you don't need
|
|
31
|
+
bidirectional communication or conversation management. For interactive,
|
|
32
|
+
stateful conversations, use CodeBuddySDKClient instead.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
prompt: The prompt to send to CodeBuddy. Can be a string for single-shot
|
|
36
|
+
queries or an AsyncIterable[dict] for streaming mode.
|
|
37
|
+
options: Optional configuration (defaults to CodeBuddyAgentOptions() if None).
|
|
38
|
+
transport: Optional transport implementation. If provided, this will be used
|
|
39
|
+
instead of the default subprocess transport.
|
|
40
|
+
|
|
41
|
+
Yields:
|
|
42
|
+
Messages from the conversation.
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
```python
|
|
46
|
+
async for message in query(prompt="What is 2+2?"):
|
|
47
|
+
print(message)
|
|
48
|
+
```
|
|
49
|
+
"""
|
|
50
|
+
if options is None:
|
|
51
|
+
options = CodeBuddyAgentOptions()
|
|
52
|
+
|
|
53
|
+
os.environ["CODEBUDDY_CODE_ENTRYPOINT"] = "sdk-py"
|
|
54
|
+
|
|
55
|
+
if transport is None:
|
|
56
|
+
transport = SubprocessTransport(options=options, prompt=prompt)
|
|
57
|
+
|
|
58
|
+
await transport.connect()
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
await _send_initialize(transport, options)
|
|
62
|
+
await _send_prompt(transport, prompt)
|
|
63
|
+
|
|
64
|
+
async for line in transport.read():
|
|
65
|
+
if not line:
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
data = json.loads(line)
|
|
70
|
+
|
|
71
|
+
# Handle control requests (hooks)
|
|
72
|
+
if data.get("type") == "control_request":
|
|
73
|
+
await _handle_control_request(transport, data, options)
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
message = parse_message(data)
|
|
77
|
+
if message:
|
|
78
|
+
yield message
|
|
79
|
+
|
|
80
|
+
if isinstance(message, ResultMessage):
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
except json.JSONDecodeError:
|
|
84
|
+
continue # Ignore non-JSON lines
|
|
85
|
+
|
|
86
|
+
finally:
|
|
87
|
+
await transport.close()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def _send_initialize(transport: Transport, options: CodeBuddyAgentOptions) -> None:
|
|
91
|
+
"""Send initialization control request."""
|
|
92
|
+
hooks_config = _build_hooks_config(options.hooks) if options.hooks else None
|
|
93
|
+
agents_config = (
|
|
94
|
+
{name: asdict(agent) for name, agent in options.agents.items()}
|
|
95
|
+
if options.agents
|
|
96
|
+
else None
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# 解析 system_prompt 配置
|
|
100
|
+
system_prompt: str | None = None
|
|
101
|
+
append_system_prompt: str | None = None
|
|
102
|
+
if isinstance(options.system_prompt, str):
|
|
103
|
+
system_prompt = options.system_prompt
|
|
104
|
+
elif isinstance(options.system_prompt, AppendSystemPrompt):
|
|
105
|
+
append_system_prompt = options.system_prompt.append
|
|
106
|
+
|
|
107
|
+
request = {
|
|
108
|
+
"type": "control_request",
|
|
109
|
+
"request_id": f"init_{id(options)}",
|
|
110
|
+
"request": {
|
|
111
|
+
"subtype": "initialize",
|
|
112
|
+
"hooks": hooks_config,
|
|
113
|
+
"systemPrompt": system_prompt,
|
|
114
|
+
"appendSystemPrompt": append_system_prompt,
|
|
115
|
+
"agents": agents_config,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
await transport.write(json.dumps(request))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def _send_prompt(
|
|
122
|
+
transport: Transport, prompt: str | AsyncIterable[dict[str, Any]]
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Send user prompt."""
|
|
125
|
+
if isinstance(prompt, str):
|
|
126
|
+
message = {
|
|
127
|
+
"type": "user",
|
|
128
|
+
"session_id": "",
|
|
129
|
+
"message": {"role": "user", "content": prompt},
|
|
130
|
+
"parent_tool_use_id": None,
|
|
131
|
+
}
|
|
132
|
+
await transport.write(json.dumps(message))
|
|
133
|
+
else:
|
|
134
|
+
async for msg in prompt:
|
|
135
|
+
await transport.write(json.dumps(msg))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async def _handle_control_request(
|
|
139
|
+
transport: Transport,
|
|
140
|
+
data: dict[str, Any],
|
|
141
|
+
options: CodeBuddyAgentOptions,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Handle control request from CLI."""
|
|
144
|
+
request_id = data.get("request_id", "")
|
|
145
|
+
request = data.get("request", {})
|
|
146
|
+
subtype = request.get("subtype", "")
|
|
147
|
+
|
|
148
|
+
if subtype == "hook_callback":
|
|
149
|
+
# Handle hook callback
|
|
150
|
+
callback_id = request.get("callback_id", "")
|
|
151
|
+
hook_input = request.get("input", {})
|
|
152
|
+
tool_use_id = request.get("tool_use_id")
|
|
153
|
+
|
|
154
|
+
# Find and execute the hook
|
|
155
|
+
response = await _execute_hook(callback_id, hook_input, tool_use_id, options)
|
|
156
|
+
|
|
157
|
+
# Send response
|
|
158
|
+
control_response = {
|
|
159
|
+
"type": "control_response",
|
|
160
|
+
"response": {
|
|
161
|
+
"subtype": "success",
|
|
162
|
+
"request_id": request_id,
|
|
163
|
+
"response": response,
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
await transport.write(json.dumps(control_response))
|
|
167
|
+
|
|
168
|
+
elif subtype == "can_use_tool":
|
|
169
|
+
await _handle_permission_request(transport, request_id, request, options)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def _handle_permission_request(
|
|
173
|
+
transport: Transport,
|
|
174
|
+
request_id: str,
|
|
175
|
+
request: dict[str, Any],
|
|
176
|
+
options: CodeBuddyAgentOptions,
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Handle permission request from CLI."""
|
|
179
|
+
tool_name = request.get("tool_name", "")
|
|
180
|
+
input_data = request.get("input", {})
|
|
181
|
+
tool_use_id = request.get("tool_use_id", "")
|
|
182
|
+
agent_id = request.get("agent_id")
|
|
183
|
+
|
|
184
|
+
can_use_tool = options.can_use_tool
|
|
185
|
+
|
|
186
|
+
# Default deny if no callback provided
|
|
187
|
+
if not can_use_tool:
|
|
188
|
+
response = {
|
|
189
|
+
"type": "control_response",
|
|
190
|
+
"response": {
|
|
191
|
+
"subtype": "success",
|
|
192
|
+
"request_id": request_id,
|
|
193
|
+
"response": {
|
|
194
|
+
"allowed": False,
|
|
195
|
+
"reason": "No permission handler provided",
|
|
196
|
+
"tool_use_id": tool_use_id,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
await transport.write(json.dumps(response))
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
callback_options = CanUseToolOptions(
|
|
205
|
+
tool_use_id=tool_use_id,
|
|
206
|
+
signal=None,
|
|
207
|
+
agent_id=agent_id,
|
|
208
|
+
suggestions=request.get("permission_suggestions"),
|
|
209
|
+
blocked_path=request.get("blocked_path"),
|
|
210
|
+
decision_reason=request.get("decision_reason"),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
result = await can_use_tool(tool_name, input_data, callback_options)
|
|
214
|
+
|
|
215
|
+
if result.behavior == "allow":
|
|
216
|
+
response_data = {
|
|
217
|
+
"allowed": True,
|
|
218
|
+
"updatedInput": result.updated_input,
|
|
219
|
+
"tool_use_id": tool_use_id,
|
|
220
|
+
}
|
|
221
|
+
else:
|
|
222
|
+
response_data = {
|
|
223
|
+
"allowed": False,
|
|
224
|
+
"reason": result.message,
|
|
225
|
+
"interrupt": result.interrupt,
|
|
226
|
+
"tool_use_id": tool_use_id,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
response = {
|
|
230
|
+
"type": "control_response",
|
|
231
|
+
"response": {
|
|
232
|
+
"subtype": "success",
|
|
233
|
+
"request_id": request_id,
|
|
234
|
+
"response": response_data,
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
await transport.write(json.dumps(response))
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
response = {
|
|
241
|
+
"type": "control_response",
|
|
242
|
+
"response": {
|
|
243
|
+
"subtype": "success",
|
|
244
|
+
"request_id": request_id,
|
|
245
|
+
"response": {
|
|
246
|
+
"allowed": False,
|
|
247
|
+
"reason": str(e),
|
|
248
|
+
"tool_use_id": tool_use_id,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
await transport.write(json.dumps(response))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
async def _execute_hook(
|
|
256
|
+
callback_id: str,
|
|
257
|
+
hook_input: dict[str, Any],
|
|
258
|
+
tool_use_id: str | None,
|
|
259
|
+
options: CodeBuddyAgentOptions,
|
|
260
|
+
) -> dict[str, Any]:
|
|
261
|
+
"""Execute a hook callback."""
|
|
262
|
+
if not options.hooks:
|
|
263
|
+
return {"continue_": True}
|
|
264
|
+
|
|
265
|
+
# Parse callback_id: hook_{event}_{matcherIndex}_{hookIndex}
|
|
266
|
+
parts = callback_id.split("_")
|
|
267
|
+
if len(parts) < 4:
|
|
268
|
+
return {"continue_": True}
|
|
269
|
+
|
|
270
|
+
event = parts[1]
|
|
271
|
+
try:
|
|
272
|
+
matcher_idx = int(parts[2])
|
|
273
|
+
hook_idx = int(parts[3])
|
|
274
|
+
except ValueError:
|
|
275
|
+
return {"continue_": True}
|
|
276
|
+
|
|
277
|
+
# Find the hook
|
|
278
|
+
matchers = options.hooks.get(event) # type: ignore[arg-type]
|
|
279
|
+
if not matchers or matcher_idx >= len(matchers):
|
|
280
|
+
return {"continue_": True}
|
|
281
|
+
|
|
282
|
+
matcher = matchers[matcher_idx]
|
|
283
|
+
if hook_idx >= len(matcher.hooks):
|
|
284
|
+
return {"continue_": True}
|
|
285
|
+
|
|
286
|
+
hook = matcher.hooks[hook_idx]
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
result = await hook(hook_input, tool_use_id, {"signal": None})
|
|
290
|
+
return dict(result)
|
|
291
|
+
except Exception as e:
|
|
292
|
+
return {"continue_": False, "stopReason": str(e)}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _build_hooks_config(
|
|
296
|
+
hooks: dict[Any, list[HookMatcher]] | None,
|
|
297
|
+
) -> dict[str, list[dict[str, Any]]] | None:
|
|
298
|
+
"""Build hooks configuration for CLI."""
|
|
299
|
+
if not hooks:
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
config: dict[str, list[dict[str, Any]]] = {}
|
|
303
|
+
|
|
304
|
+
for event, matchers in hooks.items():
|
|
305
|
+
config[str(event)] = [
|
|
306
|
+
{
|
|
307
|
+
"matcher": m.matcher,
|
|
308
|
+
"hookCallbackIds": [
|
|
309
|
+
f"hook_{event}_{i}_{j}" for j, _ in enumerate(m.hooks)
|
|
310
|
+
],
|
|
311
|
+
"timeout": m.timeout,
|
|
312
|
+
}
|
|
313
|
+
for i, m in enumerate(matchers)
|
|
314
|
+
]
|
|
315
|
+
|
|
316
|
+
return config if config else None
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Transport base class for CLI communication."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Transport(ABC):
|
|
8
|
+
"""Abstract transport layer for CLI communication."""
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
async def connect(self) -> None:
|
|
12
|
+
"""Establish connection to CLI."""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def read(self) -> AsyncIterator[str]:
|
|
16
|
+
"""Read messages from CLI as an async iterator."""
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
async def write(self, data: str) -> None:
|
|
20
|
+
"""Write data to CLI."""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
async def close(self) -> None:
|
|
24
|
+
"""Close the connection."""
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Subprocess transport for CLI communication."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import AsyncIterable, AsyncIterator
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .._binary import get_cli_path
|
|
10
|
+
from ..types import AppendSystemPrompt, CodeBuddyAgentOptions
|
|
11
|
+
from .base import Transport
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SubprocessTransport(Transport):
|
|
15
|
+
"""Transport that communicates with CLI via subprocess."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
options: CodeBuddyAgentOptions,
|
|
20
|
+
prompt: str | AsyncIterable[dict[str, Any]] | None = None,
|
|
21
|
+
):
|
|
22
|
+
self.options = options
|
|
23
|
+
self.prompt = prompt
|
|
24
|
+
self._process: asyncio.subprocess.Process | None = None
|
|
25
|
+
self._closed = False
|
|
26
|
+
|
|
27
|
+
def _get_cli_path(self) -> str:
|
|
28
|
+
"""Get the path to CLI executable."""
|
|
29
|
+
# User-specified path takes highest precedence
|
|
30
|
+
if self.options.codebuddy_code_path:
|
|
31
|
+
return str(self.options.codebuddy_code_path)
|
|
32
|
+
|
|
33
|
+
# Use the binary resolver (env var -> package binary -> monorepo)
|
|
34
|
+
return get_cli_path()
|
|
35
|
+
|
|
36
|
+
def _build_args(self) -> list[str]:
|
|
37
|
+
"""Build CLI arguments from options."""
|
|
38
|
+
args = [
|
|
39
|
+
"--input-format",
|
|
40
|
+
"stream-json",
|
|
41
|
+
"--verbose",
|
|
42
|
+
"--output-format",
|
|
43
|
+
"stream-json",
|
|
44
|
+
]
|
|
45
|
+
opts = self.options
|
|
46
|
+
|
|
47
|
+
# Model options
|
|
48
|
+
if opts.model:
|
|
49
|
+
args.extend(["--model", opts.model])
|
|
50
|
+
if opts.fallback_model:
|
|
51
|
+
args.extend(["--fallback-model", opts.fallback_model])
|
|
52
|
+
|
|
53
|
+
# Permission options
|
|
54
|
+
if opts.permission_mode:
|
|
55
|
+
args.extend(["--permission-mode", opts.permission_mode])
|
|
56
|
+
|
|
57
|
+
# Turn limits
|
|
58
|
+
if opts.max_turns:
|
|
59
|
+
args.extend(["--max-turns", str(opts.max_turns)])
|
|
60
|
+
|
|
61
|
+
# Session options
|
|
62
|
+
if opts.continue_conversation:
|
|
63
|
+
args.append("--continue")
|
|
64
|
+
if opts.resume:
|
|
65
|
+
args.extend(["--resume", opts.resume])
|
|
66
|
+
if opts.fork_session:
|
|
67
|
+
args.append("--fork-session")
|
|
68
|
+
|
|
69
|
+
# Tool options
|
|
70
|
+
if opts.allowed_tools:
|
|
71
|
+
args.extend(["--allowedTools", ",".join(opts.allowed_tools)])
|
|
72
|
+
if opts.disallowed_tools:
|
|
73
|
+
args.extend(["--disallowedTools", ",".join(opts.disallowed_tools)])
|
|
74
|
+
|
|
75
|
+
# MCP options
|
|
76
|
+
if opts.mcp_servers and isinstance(opts.mcp_servers, dict):
|
|
77
|
+
args.extend(["--mcp-config", json.dumps({"mcpServers": opts.mcp_servers})])
|
|
78
|
+
|
|
79
|
+
# Settings
|
|
80
|
+
# SDK default: don't load any filesystem settings for clean environment isolation
|
|
81
|
+
# When setting_sources is explicitly provided (including empty list), use it
|
|
82
|
+
# When not provided (None), default to 'none' for SDK isolation
|
|
83
|
+
if opts.setting_sources is not None:
|
|
84
|
+
value = "none" if len(opts.setting_sources) == 0 else ",".join(opts.setting_sources)
|
|
85
|
+
args.extend(["--setting-sources", value])
|
|
86
|
+
else:
|
|
87
|
+
# SDK default behavior: no filesystem settings loaded
|
|
88
|
+
args.extend(["--setting-sources", "none"])
|
|
89
|
+
|
|
90
|
+
# Output options
|
|
91
|
+
if opts.include_partial_messages:
|
|
92
|
+
args.append("--include-partial-messages")
|
|
93
|
+
|
|
94
|
+
# System prompt options
|
|
95
|
+
if opts.system_prompt is not None:
|
|
96
|
+
if isinstance(opts.system_prompt, str):
|
|
97
|
+
args.extend(["--system-prompt", opts.system_prompt])
|
|
98
|
+
elif isinstance(opts.system_prompt, AppendSystemPrompt):
|
|
99
|
+
args.extend(["--append-system-prompt", opts.system_prompt.append])
|
|
100
|
+
|
|
101
|
+
# Extra args (custom flags)
|
|
102
|
+
for flag, value in opts.extra_args.items():
|
|
103
|
+
if value is None:
|
|
104
|
+
args.append(f"--{flag}")
|
|
105
|
+
else:
|
|
106
|
+
args.extend([f"--{flag}", value])
|
|
107
|
+
|
|
108
|
+
return args
|
|
109
|
+
|
|
110
|
+
async def connect(self) -> None:
|
|
111
|
+
"""Start the subprocess."""
|
|
112
|
+
cli_path = self._get_cli_path()
|
|
113
|
+
args = self._build_args()
|
|
114
|
+
cwd = str(self.options.cwd) if self.options.cwd else os.getcwd()
|
|
115
|
+
|
|
116
|
+
env = {
|
|
117
|
+
**os.environ,
|
|
118
|
+
**self.options.env,
|
|
119
|
+
"CODEBUDDY_CODE_ENTRYPOINT": "sdk-py",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
self._process = await asyncio.create_subprocess_exec(
|
|
123
|
+
cli_path,
|
|
124
|
+
*args,
|
|
125
|
+
stdin=asyncio.subprocess.PIPE,
|
|
126
|
+
stdout=asyncio.subprocess.PIPE,
|
|
127
|
+
stderr=asyncio.subprocess.PIPE,
|
|
128
|
+
cwd=cwd,
|
|
129
|
+
env=env,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Start stderr reader if callback provided
|
|
133
|
+
if self.options.stderr and self._process.stderr:
|
|
134
|
+
asyncio.create_task(self._read_stderr())
|
|
135
|
+
|
|
136
|
+
async def _read_stderr(self) -> None:
|
|
137
|
+
"""Read stderr and call callback."""
|
|
138
|
+
if self._process and self._process.stderr and self.options.stderr:
|
|
139
|
+
async for line in self._process.stderr:
|
|
140
|
+
self.options.stderr(line.decode())
|
|
141
|
+
|
|
142
|
+
async def read(self) -> AsyncIterator[str]:
|
|
143
|
+
"""Read lines from stdout."""
|
|
144
|
+
if not self._process or not self._process.stdout:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
async for line in self._process.stdout:
|
|
148
|
+
if self._closed:
|
|
149
|
+
break
|
|
150
|
+
yield line.decode().strip()
|
|
151
|
+
|
|
152
|
+
async def write(self, data: str) -> None:
|
|
153
|
+
"""Write data to stdin."""
|
|
154
|
+
if self._process and self._process.stdin:
|
|
155
|
+
self._process.stdin.write((data + "\n").encode())
|
|
156
|
+
await self._process.stdin.drain()
|
|
157
|
+
|
|
158
|
+
async def close(self) -> None:
|
|
159
|
+
"""Close the subprocess."""
|
|
160
|
+
if self._closed:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
self._closed = True
|
|
164
|
+
|
|
165
|
+
if self._process:
|
|
166
|
+
self._process.kill()
|
|
167
|
+
await self._process.wait()
|