scitex-linter 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.
- scitex_linter/__init__.py +3 -0
- scitex_linter/_mcp/__init__.py +1 -0
- scitex_linter/_mcp/tools/__init__.py +8 -0
- scitex_linter/_mcp/tools/lint.py +67 -0
- scitex_linter/_server.py +23 -0
- scitex_linter/checker.py +469 -0
- scitex_linter/cli.py +449 -0
- scitex_linter/flake8_plugin.py +34 -0
- scitex_linter/formatter.py +95 -0
- scitex_linter/rules.py +384 -0
- scitex_linter/runner.py +72 -0
- scitex_linter-0.1.0.dist-info/METADATA +277 -0
- scitex_linter-0.1.0.dist-info/RECORD +17 -0
- scitex_linter-0.1.0.dist-info/WHEEL +5 -0
- scitex_linter-0.1.0.dist-info/entry_points.txt +5 -0
- scitex_linter-0.1.0.dist-info/licenses/LICENSE +661 -0
- scitex_linter-0.1.0.dist-info/top_level.txt +1 -0
scitex_linter/rules.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""Rule definitions for SciTeX linter."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class Rule:
|
|
8
|
+
id: str
|
|
9
|
+
severity: str # "error", "warning", "info"
|
|
10
|
+
category: str # "structure", "import", "io", "plot", "stats"
|
|
11
|
+
message: str
|
|
12
|
+
suggestion: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# =============================================================================
|
|
16
|
+
# Category S: Structure
|
|
17
|
+
# =============================================================================
|
|
18
|
+
|
|
19
|
+
S001 = Rule(
|
|
20
|
+
id="STX-S001",
|
|
21
|
+
severity="error",
|
|
22
|
+
category="structure",
|
|
23
|
+
message="Missing @stx.session decorator on main function",
|
|
24
|
+
suggestion=(
|
|
25
|
+
"Add @stx.session to enable reproducible session tracking, "
|
|
26
|
+
"auto-CLI, and provenance.\n"
|
|
27
|
+
" @stx.session\n"
|
|
28
|
+
" def main(...):\n"
|
|
29
|
+
" return 0"
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
S002 = Rule(
|
|
34
|
+
id="STX-S002",
|
|
35
|
+
severity="error",
|
|
36
|
+
category="structure",
|
|
37
|
+
message="Missing `if __name__ == '__main__'` guard",
|
|
38
|
+
suggestion="Add `if __name__ == '__main__': main()` at the end of the script.",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
S003 = Rule(
|
|
42
|
+
id="STX-S003",
|
|
43
|
+
severity="error",
|
|
44
|
+
category="structure",
|
|
45
|
+
message="argparse detected — @stx.session auto-generates CLI from function signature",
|
|
46
|
+
suggestion=(
|
|
47
|
+
"Remove `import argparse` and define parameters as function arguments:\n"
|
|
48
|
+
" @stx.session\n"
|
|
49
|
+
" def main(data_path: str, threshold: float = 0.5):\n"
|
|
50
|
+
" # Auto-generates: --data-path, --threshold"
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
S004 = Rule(
|
|
55
|
+
id="STX-S004",
|
|
56
|
+
severity="warning",
|
|
57
|
+
category="structure",
|
|
58
|
+
message="@stx.session function should return an integer exit code",
|
|
59
|
+
suggestion="Add `return 0` for success at the end of your session function.",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
S005 = Rule(
|
|
63
|
+
id="STX-S005",
|
|
64
|
+
severity="warning",
|
|
65
|
+
category="structure",
|
|
66
|
+
message="Missing `import scitex as stx`",
|
|
67
|
+
suggestion="Add `import scitex as stx` to use SciTeX modules.",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# =============================================================================
|
|
72
|
+
# Category I: Imports
|
|
73
|
+
# =============================================================================
|
|
74
|
+
|
|
75
|
+
I001 = Rule(
|
|
76
|
+
id="STX-I001",
|
|
77
|
+
severity="warning",
|
|
78
|
+
category="import",
|
|
79
|
+
message="Use `stx.plt` instead of importing matplotlib.pyplot directly",
|
|
80
|
+
suggestion="Replace with `stx.plt` (or `plt` injected by @stx.session).",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
I002 = Rule(
|
|
84
|
+
id="STX-I002",
|
|
85
|
+
severity="warning",
|
|
86
|
+
category="import",
|
|
87
|
+
message="Use `stx.stats` instead of importing scipy.stats directly",
|
|
88
|
+
suggestion="Replace with `stx.stats` which adds effect sizes, CI, and power analysis.",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
I003 = Rule(
|
|
92
|
+
id="STX-I003",
|
|
93
|
+
severity="warning",
|
|
94
|
+
category="import",
|
|
95
|
+
message="Use `stx.io` instead of pickle for file I/O",
|
|
96
|
+
suggestion="Replace with `stx.io.save(obj, 'file.pkl')` / `stx.io.load('file.pkl')`.",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
I004 = Rule(
|
|
100
|
+
id="STX-I004",
|
|
101
|
+
severity="warning",
|
|
102
|
+
category="import",
|
|
103
|
+
message="Use `stx.io` for CSV/DataFrame I/O instead of pandas I/O functions",
|
|
104
|
+
suggestion="Replace `pd.read_csv()` with `stx.io.load()`, `df.to_csv()` with `stx.io.save()`.",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
I005 = Rule(
|
|
108
|
+
id="STX-I005",
|
|
109
|
+
severity="warning",
|
|
110
|
+
category="import",
|
|
111
|
+
message="Use `stx.io` for array I/O instead of numpy save/load",
|
|
112
|
+
suggestion="Replace `np.save()`/`np.load()` with `stx.io.save()`/`stx.io.load()`.",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
I006 = Rule(
|
|
116
|
+
id="STX-I006",
|
|
117
|
+
severity="info",
|
|
118
|
+
category="import",
|
|
119
|
+
message="Use `rngg` (injected by @stx.session) for reproducible randomness",
|
|
120
|
+
suggestion="Remove `import random` and use `rngg` from @stx.session injection.",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
I007 = Rule(
|
|
124
|
+
id="STX-I007",
|
|
125
|
+
severity="warning",
|
|
126
|
+
category="import",
|
|
127
|
+
message="Use `logger` (injected by @stx.session) instead of logging module",
|
|
128
|
+
suggestion="Remove `import logging` and use `logger` from @stx.session injection.",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# =============================================================================
|
|
133
|
+
# Category IO: Call-level I/O (Phase 2)
|
|
134
|
+
# =============================================================================
|
|
135
|
+
|
|
136
|
+
IO001 = Rule(
|
|
137
|
+
id="STX-IO001",
|
|
138
|
+
severity="warning",
|
|
139
|
+
category="io",
|
|
140
|
+
message="`np.save()` detected — use `stx.io.save()` for provenance tracking",
|
|
141
|
+
suggestion="Replace `np.save(path, arr)` with `stx.io.save(arr, path)`.",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
IO002 = Rule(
|
|
145
|
+
id="STX-IO002",
|
|
146
|
+
severity="warning",
|
|
147
|
+
category="io",
|
|
148
|
+
message="`np.load()` detected — use `stx.io.load()` for provenance tracking",
|
|
149
|
+
suggestion="Replace `np.load(path)` with `stx.io.load(path)`.",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
IO003 = Rule(
|
|
153
|
+
id="STX-IO003",
|
|
154
|
+
severity="warning",
|
|
155
|
+
category="io",
|
|
156
|
+
message="`pd.read_csv()` detected — use `stx.io.load()` for provenance tracking",
|
|
157
|
+
suggestion="Replace `pd.read_csv(path)` with `stx.io.load(path)`.",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
IO004 = Rule(
|
|
161
|
+
id="STX-IO004",
|
|
162
|
+
severity="warning",
|
|
163
|
+
category="io",
|
|
164
|
+
message="`.to_csv()` detected — use `stx.io.save()` for provenance tracking",
|
|
165
|
+
suggestion="Replace `df.to_csv(path)` with `stx.io.save(df, path)`.",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
IO005 = Rule(
|
|
169
|
+
id="STX-IO005",
|
|
170
|
+
severity="warning",
|
|
171
|
+
category="io",
|
|
172
|
+
message="`pickle.dump()` detected — use `stx.io.save()` for provenance tracking",
|
|
173
|
+
suggestion="Replace `pickle.dump(obj, f)` with `stx.io.save(obj, 'file.pkl')`.",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
IO006 = Rule(
|
|
177
|
+
id="STX-IO006",
|
|
178
|
+
severity="warning",
|
|
179
|
+
category="io",
|
|
180
|
+
message="`json.dump()` detected — use `stx.io.save()` for provenance tracking",
|
|
181
|
+
suggestion="Replace `json.dump(obj, f)` with `stx.io.save(obj, 'file.json')`.",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
IO007 = Rule(
|
|
185
|
+
id="STX-IO007",
|
|
186
|
+
severity="warning",
|
|
187
|
+
category="io",
|
|
188
|
+
message="`plt.savefig()` detected — use `stx.io.save(fig, path)` for metadata embedding",
|
|
189
|
+
suggestion="Replace `plt.savefig(path)` with `stx.io.save(fig, path)`.",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# =============================================================================
|
|
194
|
+
# Category P: Plotting (Phase 2)
|
|
195
|
+
# =============================================================================
|
|
196
|
+
|
|
197
|
+
P001 = Rule(
|
|
198
|
+
id="STX-P001",
|
|
199
|
+
severity="info",
|
|
200
|
+
category="plot",
|
|
201
|
+
message="`ax.plot()` — consider `ax.stx_line()` for automatic CSV data export",
|
|
202
|
+
suggestion="Replace `ax.plot(x, y)` with `ax.stx_line(x, y)` for tracked plotting.",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
P002 = Rule(
|
|
206
|
+
id="STX-P002",
|
|
207
|
+
severity="info",
|
|
208
|
+
category="plot",
|
|
209
|
+
message="`ax.scatter()` — consider `ax.stx_scatter()` for automatic CSV data export",
|
|
210
|
+
suggestion="Replace `ax.scatter(x, y)` with `ax.stx_scatter(x, y)` for tracked plotting.",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
P003 = Rule(
|
|
214
|
+
id="STX-P003",
|
|
215
|
+
severity="info",
|
|
216
|
+
category="plot",
|
|
217
|
+
message="`ax.bar()` — consider `ax.stx_bar()` for automatic sample size annotation",
|
|
218
|
+
suggestion="Replace `ax.bar(x, y)` with `ax.stx_bar(x, y)` for tracked plotting.",
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
P004 = Rule(
|
|
222
|
+
id="STX-P004",
|
|
223
|
+
severity="info",
|
|
224
|
+
category="plot",
|
|
225
|
+
message="`plt.show()` is non-reproducible in batch/CI environments",
|
|
226
|
+
suggestion="Remove `plt.show()` — figures are auto-saved in session output directory.",
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
P005 = Rule(
|
|
230
|
+
id="STX-P005",
|
|
231
|
+
severity="info",
|
|
232
|
+
category="plot",
|
|
233
|
+
message="`print()` inside @stx.session — use `logger` for tracked logging",
|
|
234
|
+
suggestion="Replace `print(msg)` with `logger.info(msg)` (injected by @stx.session).",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# =============================================================================
|
|
239
|
+
# Category ST: Statistics (Phase 2)
|
|
240
|
+
# =============================================================================
|
|
241
|
+
|
|
242
|
+
ST001 = Rule(
|
|
243
|
+
id="STX-ST001",
|
|
244
|
+
severity="warning",
|
|
245
|
+
category="stats",
|
|
246
|
+
message="`scipy.stats.ttest_ind()` — use `stx.stats.ttest_ind()` for auto effect size + CI",
|
|
247
|
+
suggestion="Replace with `stx.stats.ttest_ind(a, b)` which includes Cohen's d and CI.",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
ST002 = Rule(
|
|
251
|
+
id="STX-ST002",
|
|
252
|
+
severity="warning",
|
|
253
|
+
category="stats",
|
|
254
|
+
message="`scipy.stats.mannwhitneyu()` — use `stx.stats.mannwhitneyu()` for auto effect size",
|
|
255
|
+
suggestion="Replace with `stx.stats.mannwhitneyu(a, b)` which includes Cliff's delta.",
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
ST003 = Rule(
|
|
259
|
+
id="STX-ST003",
|
|
260
|
+
severity="warning",
|
|
261
|
+
category="stats",
|
|
262
|
+
message="`scipy.stats.pearsonr()` — use `stx.stats.pearsonr()` for auto CI + power",
|
|
263
|
+
suggestion="Replace with `stx.stats.pearsonr(a, b)` which includes CI and power analysis.",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
ST004 = Rule(
|
|
267
|
+
id="STX-ST004",
|
|
268
|
+
severity="warning",
|
|
269
|
+
category="stats",
|
|
270
|
+
message="`scipy.stats.f_oneway()` — use `stx.stats.anova_oneway()` for post-hoc + effect sizes",
|
|
271
|
+
suggestion="Replace with `stx.stats.anova_oneway(*groups)` which includes eta-squared.",
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
ST005 = Rule(
|
|
275
|
+
id="STX-ST005",
|
|
276
|
+
severity="warning",
|
|
277
|
+
category="stats",
|
|
278
|
+
message="`scipy.stats.wilcoxon()` — use `stx.stats.wilcoxon()` for auto effect size",
|
|
279
|
+
suggestion="Replace with `stx.stats.wilcoxon(a, b)` which includes effect size and CI.",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
ST006 = Rule(
|
|
283
|
+
id="STX-ST006",
|
|
284
|
+
severity="warning",
|
|
285
|
+
category="stats",
|
|
286
|
+
message="`scipy.stats.kruskal()` — use `stx.stats.kruskal()` for post-hoc + effect sizes",
|
|
287
|
+
suggestion="Replace with `stx.stats.kruskal(*groups)` which includes epsilon-squared.",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# =============================================================================
|
|
292
|
+
# Category PA: Path Handling
|
|
293
|
+
# =============================================================================
|
|
294
|
+
|
|
295
|
+
PA001 = Rule(
|
|
296
|
+
id="STX-PA001",
|
|
297
|
+
severity="warning",
|
|
298
|
+
category="path",
|
|
299
|
+
message="Absolute path in `stx.io` call — use relative paths for reproducibility",
|
|
300
|
+
suggestion="Use `stx.io.save(obj, './relative/path.ext')` — paths resolve to script_out/.",
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
PA002 = Rule(
|
|
304
|
+
id="STX-PA002",
|
|
305
|
+
severity="warning",
|
|
306
|
+
category="path",
|
|
307
|
+
message="`open()` detected — use `stx.io.save()`/`stx.io.load()` which includes auto-logging",
|
|
308
|
+
suggestion=(
|
|
309
|
+
"Replace `open(path)` with `stx.io.load(path)` or `stx.io.save(obj, path)`.\n"
|
|
310
|
+
" stx.io automatically logs all I/O operations for provenance tracking."
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
PA003 = Rule(
|
|
315
|
+
id="STX-PA003",
|
|
316
|
+
severity="info",
|
|
317
|
+
category="path",
|
|
318
|
+
message="`os.makedirs()`/`mkdir()` detected — `stx.io.save()` creates directories automatically",
|
|
319
|
+
suggestion=(
|
|
320
|
+
"Remove manual directory creation.\n"
|
|
321
|
+
" `stx.io.save(obj, './subdir/file.ext')` auto-creates `subdir/` inside script_out/."
|
|
322
|
+
),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
PA004 = Rule(
|
|
326
|
+
id="STX-PA004",
|
|
327
|
+
severity="warning",
|
|
328
|
+
category="path",
|
|
329
|
+
message="`os.chdir()` detected — scripts should be run from project root",
|
|
330
|
+
suggestion="Remove `os.chdir()` and run scripts from the project root directory.",
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
PA005 = Rule(
|
|
334
|
+
id="STX-PA005",
|
|
335
|
+
severity="info",
|
|
336
|
+
category="path",
|
|
337
|
+
message="Path without `./` prefix in `stx.io` call — use `./` for explicit relative intent",
|
|
338
|
+
suggestion="Use `./filename.ext` instead of `filename.ext` to clarify relative path intent.",
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# All rules indexed by ID
|
|
343
|
+
ALL_RULES = {
|
|
344
|
+
r.id: r
|
|
345
|
+
for r in [
|
|
346
|
+
S001,
|
|
347
|
+
S002,
|
|
348
|
+
S003,
|
|
349
|
+
S004,
|
|
350
|
+
S005,
|
|
351
|
+
I001,
|
|
352
|
+
I002,
|
|
353
|
+
I003,
|
|
354
|
+
I004,
|
|
355
|
+
I005,
|
|
356
|
+
I006,
|
|
357
|
+
I007,
|
|
358
|
+
IO001,
|
|
359
|
+
IO002,
|
|
360
|
+
IO003,
|
|
361
|
+
IO004,
|
|
362
|
+
IO005,
|
|
363
|
+
IO006,
|
|
364
|
+
IO007,
|
|
365
|
+
P001,
|
|
366
|
+
P002,
|
|
367
|
+
P003,
|
|
368
|
+
P004,
|
|
369
|
+
P005,
|
|
370
|
+
ST001,
|
|
371
|
+
ST002,
|
|
372
|
+
ST003,
|
|
373
|
+
ST004,
|
|
374
|
+
ST005,
|
|
375
|
+
ST006,
|
|
376
|
+
PA001,
|
|
377
|
+
PA002,
|
|
378
|
+
PA003,
|
|
379
|
+
PA004,
|
|
380
|
+
PA005,
|
|
381
|
+
]
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
SEVERITY_ORDER = {"error": 2, "warning": 1, "info": 0}
|
scitex_linter/runner.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Run a Python script after linting it.
|
|
2
|
+
|
|
3
|
+
Core function used by the `scitex-linter python` subcommand.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from .checker import lint_file
|
|
11
|
+
from .formatter import format_issue, format_summary
|
|
12
|
+
from .rules import SEVERITY_ORDER
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _is_git_root() -> bool:
|
|
16
|
+
"""Check if the current working directory is a git repository root."""
|
|
17
|
+
return os.path.isdir(os.path.join(os.getcwd(), ".git"))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_script(filepath: str, strict: bool = False, script_args: list = None) -> int:
|
|
21
|
+
"""Lint a script then execute it.
|
|
22
|
+
|
|
23
|
+
Returns the subprocess return code, or 2 if strict mode blocks execution.
|
|
24
|
+
"""
|
|
25
|
+
if script_args is None:
|
|
26
|
+
script_args = []
|
|
27
|
+
|
|
28
|
+
# Check if running from git root
|
|
29
|
+
use_color = sys.stderr.isatty()
|
|
30
|
+
if not _is_git_root():
|
|
31
|
+
hint = "\033[94mInfo\033[0m" if use_color else "Info"
|
|
32
|
+
print(
|
|
33
|
+
f"{hint}: not running from a git root directory (cwd: {os.getcwd()})",
|
|
34
|
+
file=sys.stderr,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Lint
|
|
38
|
+
issues = lint_file(filepath)
|
|
39
|
+
|
|
40
|
+
has_errors = any(i.rule.severity == "error" for i in issues)
|
|
41
|
+
has_warnings = any(
|
|
42
|
+
SEVERITY_ORDER[i.rule.severity] >= SEVERITY_ORDER["warning"] for i in issues
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if issues:
|
|
46
|
+
header = "\033[1mSciTeX Lint\033[0m" if use_color else "SciTeX Lint"
|
|
47
|
+
print(f"\n{header}\n", file=sys.stderr)
|
|
48
|
+
|
|
49
|
+
for issue in issues:
|
|
50
|
+
print(format_issue(issue, filepath, color=use_color), file=sys.stderr)
|
|
51
|
+
print(format_summary(issues, filepath, color=use_color), file=sys.stderr)
|
|
52
|
+
print(file=sys.stderr)
|
|
53
|
+
|
|
54
|
+
if strict and has_errors:
|
|
55
|
+
msg = "\033[91mAborted\033[0m" if use_color else "Aborted"
|
|
56
|
+
print(f"{msg}: errors found (--strict mode)\n", file=sys.stderr)
|
|
57
|
+
return 2
|
|
58
|
+
|
|
59
|
+
if not has_errors and not has_warnings:
|
|
60
|
+
ok = "\033[92mOK\033[0m" if use_color else "OK"
|
|
61
|
+
print(f"{ok} {filepath}", file=sys.stderr)
|
|
62
|
+
|
|
63
|
+
# Execute
|
|
64
|
+
sep = "\u2500" * 60
|
|
65
|
+
if use_color:
|
|
66
|
+
print(f"\n\033[90m{sep}\033[0m", file=sys.stderr)
|
|
67
|
+
else:
|
|
68
|
+
print(f"\n{sep}", file=sys.stderr)
|
|
69
|
+
|
|
70
|
+
cmd = [sys.executable, filepath] + script_args
|
|
71
|
+
result = subprocess.run(cmd)
|
|
72
|
+
return result.returncode
|