toolplane-python-client 0.1.0__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.
- toolplane/__init__.py +106 -0
- toolplane/common/__init__.py +93 -0
- toolplane/common/base_config.py +129 -0
- toolplane/common/base_connection_manager.py +171 -0
- toolplane/common/base_session_manager.py +321 -0
- toolplane/common/base_tool_manager.py +347 -0
- toolplane/common/constants.py +47 -0
- toolplane/common/utils.py +310 -0
- toolplane/core/__init__.py +67 -0
- toolplane/core/config.py +107 -0
- toolplane/core/connection.py +285 -0
- toolplane/core/errors.py +298 -0
- toolplane/core/machine.py +480 -0
- toolplane/core/request.py +775 -0
- toolplane/core/session.py +332 -0
- toolplane/core/session_context.py +514 -0
- toolplane/core/task.py +130 -0
- toolplane/core/tool.py +329 -0
- toolplane/http_core/__init__.py +37 -0
- toolplane/http_core/http_config.py +97 -0
- toolplane/http_core/http_connection.py +409 -0
- toolplane/http_core/http_machine.py +298 -0
- toolplane/http_core/http_request.py +748 -0
- toolplane/http_core/http_session.py +348 -0
- toolplane/http_core/http_session_context.py +491 -0
- toolplane/http_core/http_task.py +101 -0
- toolplane/http_core/http_tool.py +400 -0
- toolplane/interfaces/__init__.py +27 -0
- toolplane/interfaces/client_interface.py +122 -0
- toolplane/interfaces/connection_interface.py +193 -0
- toolplane/interfaces/event_interface.py +290 -0
- toolplane/interfaces/request_interface.py +439 -0
- toolplane/interfaces/session_interface.py +288 -0
- toolplane/interfaces/tool_interface.py +441 -0
- toolplane/proto/__init__.py +0 -0
- toolplane/proto/service_pb2.py +315 -0
- toolplane/proto/service_pb2_grpc.py +2240 -0
- toolplane/provider_cli.py +268 -0
- toolplane/provider_registry.py +77 -0
- toolplane/provider_runtime.py +302 -0
- toolplane/toolkits/__init__.py +0 -0
- toolplane/toolkits/standalone_tools/__init__.py +0 -0
- toolplane/toolkits/standalone_tools/create_directory.py +94 -0
- toolplane/toolkits/standalone_tools/create_file.py +124 -0
- toolplane/toolkits/standalone_tools/file_search.py +229 -0
- toolplane/toolkits/standalone_tools/grep_search.py +372 -0
- toolplane/toolkits/standalone_tools/launcher.py +146 -0
- toolplane/toolkits/standalone_tools/list_dir.py +395 -0
- toolplane/toolkits/standalone_tools/read_file.py +346 -0
- toolplane/toolkits/standalone_tools/replace_string_in_file.py +407 -0
- toolplane/toolkits/standalone_tools/run_tests.py +66 -0
- toolplane/toolkits/standalone_tools/semantic_search.py +485 -0
- toolplane/toolkits/standalone_tools/standalone_toolkit.py +979 -0
- toolplane/toolkits/standalone_tools/test_failure_analysis.py +618 -0
- toolplane/toolkits/standalone_tools/test_standalone_toolkit.py +517 -0
- toolplane/toolkits/swe/__init__.py +35 -0
- toolplane/toolkits/swe/create_directory.py +15 -0
- toolplane/toolkits/swe/create_file.py +15 -0
- toolplane/toolkits/swe/descriptions.py +273 -0
- toolplane/toolkits/swe/execute_bash.py +93 -0
- toolplane/toolkits/swe/file_editor.py +775 -0
- toolplane/toolkits/swe/file_search.py +16 -0
- toolplane/toolkits/swe/finish.py +50 -0
- toolplane/toolkits/swe/grep_search.py +19 -0
- toolplane/toolkits/swe/list_dir.py +407 -0
- toolplane/toolkits/swe/read_file.py +18 -0
- toolplane/toolkits/swe/replace_string_in_file.py +17 -0
- toolplane/toolkits/swe/search.py +260 -0
- toolplane/toolkits/swe/semantic_search.py +20 -0
- toolplane/toolkits/swe/str_replace_editor.py +647 -0
- toolplane/toolkits/swe/submit.py +29 -0
- toolplane/toolkits/swe/swe_toolkit.py +1296 -0
- toolplane/toolplane_client.py +686 -0
- toolplane/toolplane_http_client.py +681 -0
- toolplane/utils/__init__.py +3 -0
- toolplane/utils/schema.py +146 -0
- toolplane_python_client-0.1.0.dist-info/METADATA +543 -0
- toolplane_python_client-0.1.0.dist-info/RECORD +81 -0
- toolplane_python_client-0.1.0.dist-info/WHEEL +5 -0
- toolplane_python_client-0.1.0.dist-info/entry_points.txt +2 -0
- toolplane_python_client-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
##############################################################################
|
|
2
|
+
# tool definitions
|
|
3
|
+
##############################################################################
|
|
4
|
+
|
|
5
|
+
ALLOWED_STR_REPLACE_EDITOR_COMMANDS = ["view", "create", "str_replace", "insert"]
|
|
6
|
+
|
|
7
|
+
# V1 File Editor
|
|
8
|
+
_FILE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files
|
|
9
|
+
* State is persistent across command calls and discussions with the user
|
|
10
|
+
* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
|
|
11
|
+
* The `create` command cannot be used if the specified `path` already exists as a file
|
|
12
|
+
* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`
|
|
13
|
+
* The `undo_edit` command will revert the last edit made to the file at `path`
|
|
14
|
+
|
|
15
|
+
Notes for using the `str_replace` command:
|
|
16
|
+
* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
|
|
17
|
+
* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique
|
|
18
|
+
* The `new_str` parameter should contain the edited lines that should replace the `old_str`
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
file_editor = {
|
|
22
|
+
"type": "function",
|
|
23
|
+
"function": {
|
|
24
|
+
"name": "file_editor",
|
|
25
|
+
"description": _FILE_EDITOR_DESCRIPTION,
|
|
26
|
+
"parameters": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"command": {
|
|
30
|
+
"description": "The command to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.",
|
|
31
|
+
"enum": ["view", "create", "str_replace", "insert", "undo_edit"],
|
|
32
|
+
"type": "string",
|
|
33
|
+
},
|
|
34
|
+
"path": {
|
|
35
|
+
"description": "Absolute path to file or directory, e.g. `/testbed/file.py` or `/testbed`.",
|
|
36
|
+
"type": "string",
|
|
37
|
+
},
|
|
38
|
+
"file_text": {
|
|
39
|
+
"description": "Required for the `create` command, contains the content of the file to be created.",
|
|
40
|
+
"type": "string",
|
|
41
|
+
},
|
|
42
|
+
"old_str": {
|
|
43
|
+
"description": "Required for the `str_replace` command, specifies the string in `path` to replace.",
|
|
44
|
+
"type": "string",
|
|
45
|
+
},
|
|
46
|
+
"new_str": {
|
|
47
|
+
"description": "Optional for the `str_replace` command to specify the replacement string. Required for the `insert` command to specify the string to insert.",
|
|
48
|
+
"type": "string",
|
|
49
|
+
},
|
|
50
|
+
"insert_line": {
|
|
51
|
+
"description": "Required for the `insert` command. The `new_str` will be inserted AFTER the line specified.",
|
|
52
|
+
"type": "integer",
|
|
53
|
+
},
|
|
54
|
+
"view_range": {
|
|
55
|
+
"description": "Optional for the `view` command when `path` points to a file. Specifies the line range to view. E.g., [11, 12] shows lines 11 and 12. Indexing starts at 1. Use [start_line, -1] to show all lines from `start_line` to the end.",
|
|
56
|
+
"type": "array",
|
|
57
|
+
"items": {"type": "integer"},
|
|
58
|
+
},
|
|
59
|
+
"concise": {
|
|
60
|
+
"description": "Optional for the `view` command. If `True`, displays a concise skeletal view of the file. Very useful for localization tasks. Highly recommended for large files.",
|
|
61
|
+
"type": "boolean",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
"required": ["command", "path"],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files
|
|
71
|
+
* State is persistent across command calls and discussions with the user
|
|
72
|
+
* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
|
|
73
|
+
* The `create` command cannot be used if the specified `path` already exists as a file
|
|
74
|
+
* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`
|
|
75
|
+
|
|
76
|
+
Notes for using the `str_replace` command:
|
|
77
|
+
* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
|
|
78
|
+
* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique
|
|
79
|
+
* The `new_str` parameter should contain the edited lines that should replace the `old_str`
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
str_replace_editor_tool = {
|
|
83
|
+
"type": "function",
|
|
84
|
+
"function": {
|
|
85
|
+
"name": "str_replace_editor",
|
|
86
|
+
"description": _STR_REPLACE_EDITOR_DESCRIPTION,
|
|
87
|
+
"parameters": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"properties": {
|
|
90
|
+
"command": {
|
|
91
|
+
"description": f"The command to run. Allowed options are: {', '.join(f'`{cmd}`' for cmd in ALLOWED_STR_REPLACE_EDITOR_COMMANDS)}.",
|
|
92
|
+
"enum": ALLOWED_STR_REPLACE_EDITOR_COMMANDS,
|
|
93
|
+
"type": "string",
|
|
94
|
+
},
|
|
95
|
+
"path": {
|
|
96
|
+
"description": "Absolute path to file or directory, e.g. `/testbed/file.py` or `/testbed`.",
|
|
97
|
+
"type": "string",
|
|
98
|
+
},
|
|
99
|
+
"file_text": {
|
|
100
|
+
"description": "Required for the `create` command, contains the content of the file to be created.",
|
|
101
|
+
"type": "string",
|
|
102
|
+
},
|
|
103
|
+
"old_str": {
|
|
104
|
+
"description": "Required for the `str_replace` command, specifies the string in `path` to replace.",
|
|
105
|
+
"type": "string",
|
|
106
|
+
},
|
|
107
|
+
"new_str": {
|
|
108
|
+
"description": "Optional for the `str_replace` command to specify the replacement string. Required for the `insert` command to specify the string to insert.",
|
|
109
|
+
"type": "string",
|
|
110
|
+
},
|
|
111
|
+
"insert_line": {
|
|
112
|
+
"description": "Required for the `insert` command. The `new_str` will be inserted AFTER the line specified.",
|
|
113
|
+
"type": "integer",
|
|
114
|
+
},
|
|
115
|
+
"view_range": {
|
|
116
|
+
"description": "Optional for the `view` command when `path` points to a file. Specifies the line range to view. E.g., [11, 12] shows lines 11 and 12. Indexing starts at 1. Use [start_line, -1] to show all lines from `start_line` to the end.",
|
|
117
|
+
"type": "array",
|
|
118
|
+
"items": {"type": "integer"},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
"required": ["command", "path"],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
_R2EGYM_BASH_EXECUTE_DESCRIPTION = """
|
|
128
|
+
Description: Execute a bash command in the terminal.
|
|
129
|
+
|
|
130
|
+
Parameters:
|
|
131
|
+
(1) command (string, required): The bash command to execute. For example: `python my_script.py`
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
r2egym_bash_execute_tool = {
|
|
135
|
+
"type": "function",
|
|
136
|
+
"function": {
|
|
137
|
+
"name": "execute_bash",
|
|
138
|
+
"description": _R2EGYM_BASH_EXECUTE_DESCRIPTION,
|
|
139
|
+
"parameters": {
|
|
140
|
+
"type": "object",
|
|
141
|
+
"properties": {
|
|
142
|
+
"cmd": {
|
|
143
|
+
"type": "string",
|
|
144
|
+
"description": "The command (and optional arguments) to execute. For example: 'python my_script.py'",
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"required": ["cmd"],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
_BASH_DESCRIPTION = """
|
|
154
|
+
Description: Execute a bash command in the terminal. Note: all commands will be executed in the current working directory: {PWD}
|
|
155
|
+
|
|
156
|
+
WARNING: UNSANDBOXED. Commands run with the full privileges of the provider
|
|
157
|
+
process and there is no command filtering. This tool is only safe to register
|
|
158
|
+
when the provider itself runs inside an isolated container or VM. Commands
|
|
159
|
+
are killed after TOOLPLANE_BASH_TIMEOUT_SECONDS (default 120).
|
|
160
|
+
|
|
161
|
+
Parameters:
|
|
162
|
+
(1) command (string, optional): The bash command to execute. For example: `python my_script.py`. If not provided, will show help.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
execute_bash_tool = {
|
|
166
|
+
"type": "function",
|
|
167
|
+
"function": {
|
|
168
|
+
"name": "execute_bash",
|
|
169
|
+
"description": _BASH_DESCRIPTION,
|
|
170
|
+
"parameters": {
|
|
171
|
+
"type": "object",
|
|
172
|
+
"properties": {
|
|
173
|
+
"command": {
|
|
174
|
+
"type": "string",
|
|
175
|
+
"description": "The command (and optional arguments) to execute. For example: 'python my_script.py'",
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
"required": [],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
_SEARCH_DESCRIPTION = """
|
|
185
|
+
Description: Search for a term in either a directory or a single file.
|
|
186
|
+
|
|
187
|
+
Behavior:
|
|
188
|
+
* If `--path` points to a directory (default is `.`), we recursively search all non-hidden files and directories.
|
|
189
|
+
* If `--path` points to a file, we run `grep -n` on that file to find line numbers containing the search term.
|
|
190
|
+
* If more than 100 files match (directory search scenario), the tool will stop listing and inform you to narrow your search.
|
|
191
|
+
* If no files are found that match your search term, the tool will inform you of that as well.
|
|
192
|
+
|
|
193
|
+
**Parameters:**
|
|
194
|
+
1. **search_term** (`string`, required): The term to search for in files.
|
|
195
|
+
2. **path** (`string`, optional): The file or directory in which to search. If not provided, defaults to the current directory (i.e., `.`).
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
search_tool = {
|
|
199
|
+
"type": "function",
|
|
200
|
+
"function": {
|
|
201
|
+
"name": "search",
|
|
202
|
+
"description": _SEARCH_DESCRIPTION,
|
|
203
|
+
"parameters": {
|
|
204
|
+
"type": "object",
|
|
205
|
+
"properties": {
|
|
206
|
+
"search_term": {
|
|
207
|
+
"description": "The term to search for in files.",
|
|
208
|
+
"type": "string",
|
|
209
|
+
},
|
|
210
|
+
"path": {
|
|
211
|
+
"description": "The file or directory to search in. Defaults to `.` if not specified.",
|
|
212
|
+
"type": "string",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
"required": ["search_term"],
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# V1 Finish
|
|
221
|
+
_FINISH_DESCRIPTION = """
|
|
222
|
+
"A simple finish tool with a 'submit' command.\n\n"
|
|
223
|
+
"Notes about the `submit` command:\n"
|
|
224
|
+
"* When invoked with `--result`, the provided string is used for submitting required task results.\n"
|
|
225
|
+
"* If no `--result` is provided, it defaults to an empty string.\n\n"
|
|
226
|
+
"**Parameters:**\n"
|
|
227
|
+
" 1. **command** (`string`, required): The command to run. Currently allowed option is: `submit`.\n"
|
|
228
|
+
" - Allowed value: [`submit`]\n"
|
|
229
|
+
" 2. **result** (`string`, optional): The result text to submit. Defaults to an empty string.\n"
|
|
230
|
+
"""
|
|
231
|
+
finish_tool = {
|
|
232
|
+
"type": "function",
|
|
233
|
+
"function": {
|
|
234
|
+
"name": "finish",
|
|
235
|
+
"description": _FINISH_DESCRIPTION,
|
|
236
|
+
"parameters": {
|
|
237
|
+
"type": "object",
|
|
238
|
+
"properties": {
|
|
239
|
+
"command": {
|
|
240
|
+
"description": "The command to run. Currently only `submit` is supported.",
|
|
241
|
+
"type": "string",
|
|
242
|
+
"enum": ["submit"],
|
|
243
|
+
},
|
|
244
|
+
"result": {
|
|
245
|
+
"description": "Optional. The result text to submit. Defaults to an empty string if not provided.",
|
|
246
|
+
"type": "string",
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
"required": ["command"],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# V2 Submit
|
|
255
|
+
_SUBMIT_DESCRIPTION = """
|
|
256
|
+
A simple submit tool to finish tasks.
|
|
257
|
+
|
|
258
|
+
This tool signals completion of a task or submission of results.
|
|
259
|
+
No parameters required - simply call to indicate task completion.
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
submit_tool = {
|
|
263
|
+
"type": "function",
|
|
264
|
+
"function": {
|
|
265
|
+
"name": "submit",
|
|
266
|
+
"description": _SUBMIT_DESCRIPTION,
|
|
267
|
+
"parameters": {
|
|
268
|
+
"type": "object",
|
|
269
|
+
"properties": {},
|
|
270
|
+
"required": [],
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Description: Execute a bash command in the terminal.
|
|
4
|
+
|
|
5
|
+
WARNING: this tool provides NO sandboxing. It runs model-supplied shell
|
|
6
|
+
commands with the full privileges of the provider process. Only register it
|
|
7
|
+
on a provider that is itself isolated (container or VM). The former
|
|
8
|
+
first-token command blocklist was deliberately removed: such filters are
|
|
9
|
+
trivially bypassed (absolute paths, env assignments, `sh -c`, ...) and
|
|
10
|
+
provided a false sense of guardrail while blocking nothing.
|
|
11
|
+
|
|
12
|
+
Environment:
|
|
13
|
+
TOOLPLANE_BASH_TIMEOUT_SECONDS maximum runtime per command (default 120).
|
|
14
|
+
TOOLPLANE_WORKSPACE_ROOT when set, commands run with this working
|
|
15
|
+
directory. This is a convenience and a soft
|
|
16
|
+
boundary only — the shell itself is not
|
|
17
|
+
confined; real isolation must come from the
|
|
18
|
+
provider's container/VM.
|
|
19
|
+
|
|
20
|
+
Parameters:
|
|
21
|
+
--command (string, optional): The bash command to execute. For example: --command 'python my_script.py'. If not provided, will show help.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import os
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
DEFAULT_COMMAND_TIMEOUT_SECONDS = 120.0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def command_timeout_seconds():
|
|
33
|
+
raw = os.environ.get("TOOLPLANE_BASH_TIMEOUT_SECONDS", "").strip()
|
|
34
|
+
if not raw:
|
|
35
|
+
return DEFAULT_COMMAND_TIMEOUT_SECONDS
|
|
36
|
+
try:
|
|
37
|
+
value = float(raw)
|
|
38
|
+
except ValueError:
|
|
39
|
+
return DEFAULT_COMMAND_TIMEOUT_SECONDS
|
|
40
|
+
return value if value > 0 else DEFAULT_COMMAND_TIMEOUT_SECONDS
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def workspace_root():
|
|
44
|
+
root = os.environ.get("TOOLPLANE_WORKSPACE_ROOT", "").strip()
|
|
45
|
+
return root or None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def run_command(cmd):
|
|
49
|
+
"""Run a shell command with a hard timeout and the configured workspace cwd."""
|
|
50
|
+
return subprocess.run(
|
|
51
|
+
cmd,
|
|
52
|
+
shell=True,
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
timeout=command_timeout_seconds(),
|
|
56
|
+
cwd=workspace_root(),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main():
|
|
61
|
+
parser = argparse.ArgumentParser(description="Execute a bash command.")
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"command",
|
|
64
|
+
type=str,
|
|
65
|
+
help="The command (and optional arguments) to execute. For example: 'python my_script.py'",
|
|
66
|
+
)
|
|
67
|
+
args = parser.parse_args()
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
result = run_command(args.command)
|
|
71
|
+
except subprocess.TimeoutExpired:
|
|
72
|
+
print(
|
|
73
|
+
f"Command timed out after {command_timeout_seconds():.0f}s "
|
|
74
|
+
"(configure via TOOLPLANE_BASH_TIMEOUT_SECONDS)."
|
|
75
|
+
)
|
|
76
|
+
sys.exit(124)
|
|
77
|
+
|
|
78
|
+
if result.returncode != 0:
|
|
79
|
+
print("Error executing command:\n")
|
|
80
|
+
print("[STDOUT]\n")
|
|
81
|
+
print(result.stdout.strip(), "\n")
|
|
82
|
+
print("[STDERR]\n")
|
|
83
|
+
print(result.stderr.strip())
|
|
84
|
+
sys.exit(result.returncode)
|
|
85
|
+
|
|
86
|
+
print("[STDOUT]\n")
|
|
87
|
+
print(result.stdout.strip(), "\n")
|
|
88
|
+
print("[STDERR]\n")
|
|
89
|
+
print(result.stderr.strip())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
main()
|