scout-rig 0.1.0 → 0.2.0
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.
- checksums.yaml +4 -4
- data/.vimproject +25 -0
- data/README.md +337 -0
- data/VERSION +1 -1
- data/doc/Python.md +482 -0
- data/lib/scout/python/run.rb +16 -7
- data/lib/scout/python/script.rb +5 -0
- data/lib/scout/python.rb +19 -1
- data/lib/scout/workflow/python/inputs.rb +59 -0
- data/lib/scout/workflow/python/task.rb +110 -0
- data/lib/scout/workflow/python.rb +14 -0
- data/lib/scout-rig.rb +1 -1
- data/python/scout/__init__.py +10 -3
- data/python/scout/runner.py +385 -0
- data/scout-rig.gemspec +13 -7
- data/test/scout/python/test_run.rb +15 -0
- data/test/scout/workflow/python/test_task.rb +37 -0
- data/test/scout/workflow/test_python.rb +47 -0
- data/test/test_helper.rb +51 -1
- metadata +12 -6
- data/README.rdoc +0 -18
- data/python/scout/__pycache__/__init__.cpython-310.pyc +0 -0
- data/python/scout/__pycache__/workflow.cpython-310.pyc +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
require_relative '../../python'
|
|
2
|
+
require 'shellwords'
|
|
3
|
+
module PythonWorkflow
|
|
4
|
+
|
|
5
|
+
def self.read_python_metadata(file)
|
|
6
|
+
out = ScoutPython.run_file file, '--scout-metadata'
|
|
7
|
+
raise "Error getting metadata from #{file}: #{err}" unless out.exit_status == 0
|
|
8
|
+
JSON.parse(out.read)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.map_returns(py_type)
|
|
12
|
+
case py_type
|
|
13
|
+
when 'string' then :string
|
|
14
|
+
when 'integer' then :integer
|
|
15
|
+
when 'float' then :float
|
|
16
|
+
when 'boolean' then :boolean
|
|
17
|
+
when 'binary' then :binary
|
|
18
|
+
when 'path' then :string
|
|
19
|
+
when 'list', 'array' then :array
|
|
20
|
+
else
|
|
21
|
+
if py_type.start_with?("list[")
|
|
22
|
+
:array
|
|
23
|
+
else
|
|
24
|
+
:string
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.map_param(p)
|
|
30
|
+
desc = p['help'] || ""
|
|
31
|
+
default = p['default']
|
|
32
|
+
required = p['required'] ? true : false
|
|
33
|
+
|
|
34
|
+
ruby_type =
|
|
35
|
+
case p['type']
|
|
36
|
+
when 'string' then :string
|
|
37
|
+
when 'integer' then :integer
|
|
38
|
+
when 'float' then :float
|
|
39
|
+
when 'boolean' then :boolean
|
|
40
|
+
when 'binary' then :binary
|
|
41
|
+
when 'path' then :file
|
|
42
|
+
else
|
|
43
|
+
if p['type'].start_with?('list[')
|
|
44
|
+
subtype = p['type'][5..-2]
|
|
45
|
+
ruby_sub = case subtype
|
|
46
|
+
when 'path' then :file_array
|
|
47
|
+
else :array
|
|
48
|
+
end
|
|
49
|
+
ruby_sub
|
|
50
|
+
else
|
|
51
|
+
:string
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
options = {}
|
|
56
|
+
options[:required] = true if required
|
|
57
|
+
|
|
58
|
+
{ name: p['name'], type: ruby_type, desc: desc, default: default, options: options }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def python_task(task_sym, file: nil, returns: nil, extension: nil, desc: nil)
|
|
62
|
+
name = task_sym.to_s
|
|
63
|
+
file ||= python_task_dir[name].find_with_extension('py')
|
|
64
|
+
raise "Python task file not found: #{file}" unless File.exist?(file)
|
|
65
|
+
|
|
66
|
+
metas = PythonWorkflow.read_python_metadata(file)
|
|
67
|
+
metas = [metas] unless Array === metas
|
|
68
|
+
|
|
69
|
+
# For each function defined in the python file, register a workflow task
|
|
70
|
+
metas.each do |meta|
|
|
71
|
+
meta['returns'] = returns.to_s if returns
|
|
72
|
+
task_desc = desc || meta['description']
|
|
73
|
+
|
|
74
|
+
ruby_returns = PythonWorkflow.map_returns(meta['returns'])
|
|
75
|
+
ruby_inputs = meta['params'].map { |p| PythonWorkflow.map_param(p) }
|
|
76
|
+
|
|
77
|
+
ruby_inputs.each do |inp|
|
|
78
|
+
input(inp[:name].to_sym, inp[:type], inp[:desc], inp[:default], inp[:options] || {})
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
self.desc(task_desc) if task_desc && !task_desc.empty?
|
|
82
|
+
self.extension(extension) if extension
|
|
83
|
+
|
|
84
|
+
task({ meta['name'].to_sym => ruby_returns }) do |*args|
|
|
85
|
+
arg_names = ruby_inputs.map { |i| i[:name] }
|
|
86
|
+
values = {}
|
|
87
|
+
arg_names.each_with_index { |n,i| values[n] = args[i] }
|
|
88
|
+
|
|
89
|
+
argv = PythonWorkflow.build_python_argv(meta['params'], values)
|
|
90
|
+
# prefix with function name so the python script runs the desired function
|
|
91
|
+
full_argv = [meta['name']] + argv
|
|
92
|
+
|
|
93
|
+
out = ScoutPython.run_file file, Shellwords.shelljoin(full_argv)
|
|
94
|
+
# out is expected to respond to exit_status and read
|
|
95
|
+
raise "Python task #{meta['name']} failed" unless out.exit_status == 0
|
|
96
|
+
txt = out.read.to_s
|
|
97
|
+
# try JSON
|
|
98
|
+
begin
|
|
99
|
+
next JSON.parse(txt)
|
|
100
|
+
rescue JSON::ParserError
|
|
101
|
+
# not JSON; for list returns, split by newline
|
|
102
|
+
if ruby_returns == :array || ruby_returns == :file_array
|
|
103
|
+
next txt.split("\n").map(&:to_s)
|
|
104
|
+
end
|
|
105
|
+
next txt.strip
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'scout/workflow'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
require_relative 'python/inputs'
|
|
6
|
+
require_relative 'python/task'
|
|
7
|
+
|
|
8
|
+
module PythonWorkflow
|
|
9
|
+
attr_accessor :python_task_dir
|
|
10
|
+
|
|
11
|
+
def python_task_dir
|
|
12
|
+
@python_task_dir ||= Scout.python.task.find(:lib)
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/scout-rig.rb
CHANGED
data/python/scout/__init__.py
CHANGED
|
@@ -6,7 +6,6 @@ import shutil
|
|
|
6
6
|
import pandas
|
|
7
7
|
import numpy
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
def cmd(cmd=None):
|
|
11
10
|
if cmd is None:
|
|
12
11
|
print("Rbbt")
|
|
@@ -14,6 +13,7 @@ def cmd(cmd=None):
|
|
|
14
13
|
return subprocess.run('rbbt_exec.rb', input=cmd.encode('utf-8'), capture_output=True).stdout.decode()
|
|
15
14
|
|
|
16
15
|
|
|
16
|
+
|
|
17
17
|
def libdir():
|
|
18
18
|
return cmd('puts Rbbt.find(:lib)').rstrip()
|
|
19
19
|
|
|
@@ -188,9 +188,9 @@ def save_job_inputs(data):
|
|
|
188
188
|
return temp_dir
|
|
189
189
|
|
|
190
190
|
|
|
191
|
-
def run_job(workflow, task,
|
|
191
|
+
def run_job(workflow, task, jobname='Default', fork=False, clean=False, **kwargs):
|
|
192
192
|
inputs_dir = save_job_inputs(kwargs)
|
|
193
|
-
cmd = ['rbbt', 'workflow', 'task', workflow, task, '--jobname',
|
|
193
|
+
cmd = ['rbbt', 'workflow', 'task', workflow, task, '--jobname', jobname, '--load_inputs', inputs_dir, '--nocolor']
|
|
194
194
|
|
|
195
195
|
if fork:
|
|
196
196
|
cmd.append('--fork')
|
|
@@ -219,3 +219,10 @@ if __name__ == "__main__":
|
|
|
219
219
|
import json
|
|
220
220
|
res = run_job('Baking', 'bake_muffin_tray', 'test', add_blueberries=True, fork=True)
|
|
221
221
|
print(res)
|
|
222
|
+
|
|
223
|
+
# expose smaller task helper implemented in runner.py
|
|
224
|
+
try:
|
|
225
|
+
from .runner import task, describe_function
|
|
226
|
+
except Exception:
|
|
227
|
+
# avoid failing import if runner missing
|
|
228
|
+
pass
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import get_origin, get_args, Union, List
|
|
7
|
+
import atexit
|
|
8
|
+
|
|
9
|
+
__all__ = ["task", "describe_function"]
|
|
10
|
+
|
|
11
|
+
# registry to hold tasks defined via scout.task in a module
|
|
12
|
+
_SCOUT_TASK_REGISTRY = [] # list of (func, meta)
|
|
13
|
+
_SCOUT_DEFER_REGISTERED = set()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _python_type_to_string(ann) -> str:
|
|
17
|
+
if ann is inspect._empty:
|
|
18
|
+
return "string"
|
|
19
|
+
origin = get_origin(ann)
|
|
20
|
+
args = get_args(ann)
|
|
21
|
+
|
|
22
|
+
if origin is Union:
|
|
23
|
+
non_none = [a for a in args if a is not type(None)]
|
|
24
|
+
if len(non_none) == 1:
|
|
25
|
+
return _python_type_to_string(non_none[0])
|
|
26
|
+
return "string"
|
|
27
|
+
|
|
28
|
+
if origin in (list, List):
|
|
29
|
+
subtype = "string"
|
|
30
|
+
if args:
|
|
31
|
+
subtype = _python_type_to_string(args[0])
|
|
32
|
+
return f"list[{subtype}]"
|
|
33
|
+
|
|
34
|
+
if ann is Path:
|
|
35
|
+
return "path"
|
|
36
|
+
|
|
37
|
+
if ann in (str, bytes):
|
|
38
|
+
return "string" if ann is str else "binary"
|
|
39
|
+
if ann is int:
|
|
40
|
+
return "integer"
|
|
41
|
+
if ann is float:
|
|
42
|
+
return "float"
|
|
43
|
+
if ann is bool:
|
|
44
|
+
return "boolean"
|
|
45
|
+
|
|
46
|
+
return "string"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _required_from_default(default_val, ann) -> bool:
|
|
50
|
+
if default_val is not inspect._empty:
|
|
51
|
+
return False
|
|
52
|
+
origin = get_origin(ann)
|
|
53
|
+
args = get_args(ann)
|
|
54
|
+
if origin is Union and type(None) in args:
|
|
55
|
+
return False
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _parse_numpy_params(doc: str) -> dict:
|
|
60
|
+
if not doc:
|
|
61
|
+
return {}
|
|
62
|
+
lines = doc.splitlines()
|
|
63
|
+
params = {}
|
|
64
|
+
i = 0
|
|
65
|
+
while i < len(lines):
|
|
66
|
+
if lines[i].strip().lower() == "parameters":
|
|
67
|
+
i += 1
|
|
68
|
+
if i < len(lines) and set(lines[i].strip()) == {"-"}:
|
|
69
|
+
i += 1
|
|
70
|
+
break
|
|
71
|
+
i += 1
|
|
72
|
+
else:
|
|
73
|
+
return {}
|
|
74
|
+
|
|
75
|
+
current_name = None
|
|
76
|
+
current_desc = []
|
|
77
|
+
while i < len(lines):
|
|
78
|
+
line = lines[i]
|
|
79
|
+
if ":" in line and not line.startswith(" "):
|
|
80
|
+
if current_name:
|
|
81
|
+
params[current_name] = " ".join(d.strip() for d in current_desc).strip()
|
|
82
|
+
current_desc = []
|
|
83
|
+
current_name = line.split(":")[0].strip()
|
|
84
|
+
else:
|
|
85
|
+
if current_name is not None:
|
|
86
|
+
current_desc.append(line)
|
|
87
|
+
else:
|
|
88
|
+
break
|
|
89
|
+
i += 1
|
|
90
|
+
|
|
91
|
+
if current_name:
|
|
92
|
+
params[current_name] = " ".join(d.strip() for d in current_desc).strip()
|
|
93
|
+
|
|
94
|
+
return params
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def describe_function(func) -> dict:
|
|
98
|
+
sig = inspect.signature(func)
|
|
99
|
+
doc = inspect.getdoc(func) or ""
|
|
100
|
+
params_doc = _parse_numpy_params(doc)
|
|
101
|
+
|
|
102
|
+
ret_type = _python_type_to_string(sig.return_annotation)
|
|
103
|
+
description = doc.split("\n\n", 1)[0] if doc else ""
|
|
104
|
+
|
|
105
|
+
params = []
|
|
106
|
+
for name, p in sig.parameters.items():
|
|
107
|
+
p_type = _python_type_to_string(p.annotation)
|
|
108
|
+
default = None if p.default is inspect._empty else p.default
|
|
109
|
+
required = _required_from_default(p.default, p.annotation)
|
|
110
|
+
help_text = params_doc.get(name, "")
|
|
111
|
+
params.append({
|
|
112
|
+
"name": name,
|
|
113
|
+
"type": p_type,
|
|
114
|
+
"required": required,
|
|
115
|
+
"default": default,
|
|
116
|
+
"help": help_text,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"name": func.__name__,
|
|
121
|
+
"description": description,
|
|
122
|
+
"returns": ret_type,
|
|
123
|
+
"params": params,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _has_boolean_optional_action():
|
|
128
|
+
try:
|
|
129
|
+
_ = argparse.BooleanOptionalAction
|
|
130
|
+
return True
|
|
131
|
+
except AttributeError:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class _BooleanOptionalAction(argparse.Action):
|
|
136
|
+
def __init__(self, option_strings, dest, default=None, required=False, help=None, metavar=None):
|
|
137
|
+
_option_strings = []
|
|
138
|
+
for option in option_strings:
|
|
139
|
+
_option_strings.append(option)
|
|
140
|
+
if option.startswith("--"):
|
|
141
|
+
_option_strings.append("--no-" + option[2:])
|
|
142
|
+
super().__init__(option_strings=_option_strings, dest=dest, nargs=0,
|
|
143
|
+
const=None, default=default, required=required, help=help)
|
|
144
|
+
|
|
145
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
146
|
+
if option_string and option_string.startswith("--no-"):
|
|
147
|
+
setattr(namespace, self.dest, False)
|
|
148
|
+
else:
|
|
149
|
+
setattr(namespace, self.dest, True)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _add_arg_for_param(parser, p):
|
|
153
|
+
name = p["name"]
|
|
154
|
+
ptype = p["type"]
|
|
155
|
+
required = p["required"]
|
|
156
|
+
default = p["default"]
|
|
157
|
+
help_text = p.get("help") or None
|
|
158
|
+
|
|
159
|
+
flag = f"--{name}"
|
|
160
|
+
|
|
161
|
+
if ptype.startswith("list["):
|
|
162
|
+
subtype = ptype[5:-1] or "string"
|
|
163
|
+
py_caster = str
|
|
164
|
+
if subtype == "integer":
|
|
165
|
+
py_caster = int
|
|
166
|
+
elif subtype == "float":
|
|
167
|
+
py_caster = float
|
|
168
|
+
elif subtype == "path":
|
|
169
|
+
py_caster = lambda s: str(Path(s))
|
|
170
|
+
parser.add_argument(flag, nargs="+", type=py_caster, required=required, default=default, help=help_text)
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
if ptype == "boolean":
|
|
174
|
+
if default is True:
|
|
175
|
+
if _has_boolean_optional_action():
|
|
176
|
+
parser.add_argument(flag, action=argparse.BooleanOptionalAction, default=True, help=help_text, required=False)
|
|
177
|
+
else:
|
|
178
|
+
parser.add_argument(flag, action=_BooleanOptionalAction, default=True, help=help_text, required=False)
|
|
179
|
+
else:
|
|
180
|
+
parser.add_argument(flag, action="store_true", default=False, required=False, help=help_text)
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
py_caster = str
|
|
184
|
+
if ptype == "integer":
|
|
185
|
+
py_caster = int
|
|
186
|
+
elif ptype == "float":
|
|
187
|
+
py_caster = float
|
|
188
|
+
elif ptype == "path":
|
|
189
|
+
py_caster = lambda s: str(Path(s))
|
|
190
|
+
|
|
191
|
+
parser.add_argument(flag, type=py_caster, required=required, default=default, help=help_text)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _write_output(out_path: str, data):
|
|
195
|
+
path = Path(out_path)
|
|
196
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
197
|
+
if isinstance(data, (bytes, bytearray)):
|
|
198
|
+
path.write_bytes(data)
|
|
199
|
+
else:
|
|
200
|
+
path.write_text(str(data))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _run_cli(func, meta: dict):
|
|
204
|
+
# single-function CLI metadata
|
|
205
|
+
if "--scout-metadata" in sys.argv:
|
|
206
|
+
print(json.dumps(meta))
|
|
207
|
+
return 0
|
|
208
|
+
|
|
209
|
+
parser = argparse.ArgumentParser(description=meta.get("description") or "")
|
|
210
|
+
for p in meta["params"]:
|
|
211
|
+
_add_arg_for_param(parser, p)
|
|
212
|
+
|
|
213
|
+
parser.add_argument("--scout-output", default=None, help="Optional output file path for the result")
|
|
214
|
+
args = vars(parser.parse_args())
|
|
215
|
+
|
|
216
|
+
out_path = args.pop("scout_output", None)
|
|
217
|
+
|
|
218
|
+
kwargs = {}
|
|
219
|
+
for p in meta["params"]:
|
|
220
|
+
name = p["name"]
|
|
221
|
+
val = args.get(name)
|
|
222
|
+
kwargs[name] = val
|
|
223
|
+
|
|
224
|
+
result = func(**kwargs)
|
|
225
|
+
|
|
226
|
+
if out_path:
|
|
227
|
+
_write_output(out_path, result)
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
# Serialization rules:
|
|
231
|
+
# - bytes -> raw bytes to stdout.buffer
|
|
232
|
+
# - str/int/float/bool/None -> printed as plain string
|
|
233
|
+
# - list/tuple -> newline-separated items
|
|
234
|
+
# - others -> JSON dump
|
|
235
|
+
if isinstance(result, (bytes, bytearray)):
|
|
236
|
+
sys.stdout.buffer.write(result)
|
|
237
|
+
return 0
|
|
238
|
+
|
|
239
|
+
if result is None or isinstance(result, (str, int, float, bool)):
|
|
240
|
+
print(result if result is not None else "")
|
|
241
|
+
return 0
|
|
242
|
+
|
|
243
|
+
if isinstance(result, (list, tuple)):
|
|
244
|
+
# print each item on its own line
|
|
245
|
+
for item in result:
|
|
246
|
+
# convert None to empty string
|
|
247
|
+
if item is None:
|
|
248
|
+
print("")
|
|
249
|
+
else:
|
|
250
|
+
# bytes inside list -> decode
|
|
251
|
+
if isinstance(item, (bytes, bytearray)):
|
|
252
|
+
try:
|
|
253
|
+
sys.stdout.buffer.write(item)
|
|
254
|
+
# ensure newline
|
|
255
|
+
sys.stdout.buffer.write(b"\n")
|
|
256
|
+
except Exception:
|
|
257
|
+
print(str(item))
|
|
258
|
+
else:
|
|
259
|
+
print(item)
|
|
260
|
+
return 0
|
|
261
|
+
|
|
262
|
+
# fallback: JSON serialize (use default=str to handle Paths etc.)
|
|
263
|
+
try:
|
|
264
|
+
print(json.dumps(result, default=str, ensure_ascii=False))
|
|
265
|
+
except Exception:
|
|
266
|
+
# last resort: string representation
|
|
267
|
+
print(str(result))
|
|
268
|
+
return 0
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def task(func=None, **options):
|
|
272
|
+
if func is None:
|
|
273
|
+
raise ValueError("scout.task expects a function")
|
|
274
|
+
|
|
275
|
+
meta = describe_function(func)
|
|
276
|
+
setattr(func, "__scout_meta__", meta)
|
|
277
|
+
|
|
278
|
+
# register globally
|
|
279
|
+
_SCOUT_TASK_REGISTRY.append((func, meta))
|
|
280
|
+
|
|
281
|
+
mod = inspect.getmodule(func)
|
|
282
|
+
# If the defining module is executed as a script, defer execution until
|
|
283
|
+
# after the module is fully imported by using an atexit handler. This
|
|
284
|
+
# allows multiple scout.task(...) calls to register multiple functions.
|
|
285
|
+
if mod and getattr(mod, "__name__", None) == "__main__":
|
|
286
|
+
mod_id = id(mod)
|
|
287
|
+
if mod_id not in _SCOUT_DEFER_REGISTERED:
|
|
288
|
+
_SCOUT_DEFER_REGISTERED.add(mod_id)
|
|
289
|
+
|
|
290
|
+
def _scout_run_deferred():
|
|
291
|
+
# if metadata requested, emit all metas
|
|
292
|
+
if "--scout-metadata" in sys.argv:
|
|
293
|
+
metas = [m for (_f, m) in _SCOUT_TASK_REGISTRY]
|
|
294
|
+
print(json.dumps(metas))
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
# Determine if user requested help
|
|
298
|
+
has_help = any(a in ("-h", "--help") for a in sys.argv[1:])
|
|
299
|
+
|
|
300
|
+
# Determine function to run: optional first positional arg
|
|
301
|
+
args = sys.argv[1:]
|
|
302
|
+
func_name = None
|
|
303
|
+
if len(args) >= 1 and not args[0].startswith("-"):
|
|
304
|
+
func_name = args[0]
|
|
305
|
+
# do not pop yet; if running we will pop below
|
|
306
|
+
|
|
307
|
+
# If user requested help, show the help for the selected task and exit
|
|
308
|
+
if has_help:
|
|
309
|
+
# choose meta
|
|
310
|
+
chosen_meta = None
|
|
311
|
+
if func_name:
|
|
312
|
+
for _f, m in _SCOUT_TASK_REGISTRY:
|
|
313
|
+
if m.get('name') == func_name:
|
|
314
|
+
chosen_meta = m
|
|
315
|
+
break
|
|
316
|
+
if chosen_meta is None:
|
|
317
|
+
print(f"[scout.task] Unknown task '{func_name}'", file=sys.stderr)
|
|
318
|
+
import os
|
|
319
|
+
os._exit(2)
|
|
320
|
+
else:
|
|
321
|
+
chosen_meta = _SCOUT_TASK_REGISTRY[-1][1]
|
|
322
|
+
|
|
323
|
+
# Build a parser for that function and print help
|
|
324
|
+
parser = argparse.ArgumentParser(prog=f"{Path(sys.argv[0]).name} {chosen_meta.get('name')}", description=chosen_meta.get('description') or "")
|
|
325
|
+
for p in chosen_meta['params']:
|
|
326
|
+
_add_arg_for_param(parser, p)
|
|
327
|
+
parser.add_argument("--scout-output", default=None, help="Optional output file path for the result")
|
|
328
|
+
parser.print_help()
|
|
329
|
+
try:
|
|
330
|
+
sys.stdout.flush()
|
|
331
|
+
except Exception:
|
|
332
|
+
pass
|
|
333
|
+
try:
|
|
334
|
+
sys.stderr.flush()
|
|
335
|
+
except Exception:
|
|
336
|
+
pass
|
|
337
|
+
import os
|
|
338
|
+
os._exit(0)
|
|
339
|
+
|
|
340
|
+
chosen = None
|
|
341
|
+
if func_name:
|
|
342
|
+
for f, m in _SCOUT_TASK_REGISTRY:
|
|
343
|
+
if m.get('name') == func_name:
|
|
344
|
+
chosen = (f, m)
|
|
345
|
+
break
|
|
346
|
+
if chosen is None:
|
|
347
|
+
print(f"[scout.task] Unknown task '{func_name}'", file=sys.stderr)
|
|
348
|
+
sys.exit(2)
|
|
349
|
+
else:
|
|
350
|
+
# default: last registered task
|
|
351
|
+
chosen = _SCOUT_TASK_REGISTRY[-1]
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
# if a function name was provided as first positional arg, remove it
|
|
355
|
+
if func_name and len(sys.argv) > 1:
|
|
356
|
+
# remove the first positional argument (function name)
|
|
357
|
+
sys.argv.pop(1)
|
|
358
|
+
|
|
359
|
+
code = _run_cli(chosen[0], chosen[1])
|
|
360
|
+
try:
|
|
361
|
+
sys.stdout.flush()
|
|
362
|
+
except Exception:
|
|
363
|
+
pass
|
|
364
|
+
try:
|
|
365
|
+
sys.stderr.flush()
|
|
366
|
+
except Exception:
|
|
367
|
+
pass
|
|
368
|
+
import os
|
|
369
|
+
os._exit(code)
|
|
370
|
+
except SystemExit as e:
|
|
371
|
+
# argparse may trigger SystemExit (e.g. --help). Ensure we exit quietly.
|
|
372
|
+
try:
|
|
373
|
+
import os
|
|
374
|
+
code = e.code if isinstance(e.code, int) else 0
|
|
375
|
+
os._exit(code)
|
|
376
|
+
except Exception:
|
|
377
|
+
pass
|
|
378
|
+
except Exception as e:
|
|
379
|
+
print(f"[scout.task] Error: {e}", file=sys.stderr)
|
|
380
|
+
import os
|
|
381
|
+
os._exit(1)
|
|
382
|
+
|
|
383
|
+
atexit.register(_scout_run_deferred)
|
|
384
|
+
|
|
385
|
+
return func
|
data/scout-rig.gemspec
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
|
3
3
|
# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
|
|
4
4
|
# -*- encoding: utf-8 -*-
|
|
5
|
-
# stub: scout-rig 0.
|
|
5
|
+
# stub: scout-rig 0.2.0 ruby lib
|
|
6
6
|
|
|
7
7
|
Gem::Specification.new do |s|
|
|
8
8
|
s.name = "scout-rig".freeze
|
|
9
|
-
s.version = "0.
|
|
9
|
+
s.version = "0.2.0".freeze
|
|
10
10
|
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
|
12
12
|
s.require_paths = ["lib".freeze]
|
|
@@ -16,36 +16,42 @@ Gem::Specification.new do |s|
|
|
|
16
16
|
s.email = "mikisvaz@gmail.com".freeze
|
|
17
17
|
s.extra_rdoc_files = [
|
|
18
18
|
"LICENSE.txt",
|
|
19
|
-
"README.
|
|
19
|
+
"README.md"
|
|
20
20
|
]
|
|
21
21
|
s.files = [
|
|
22
22
|
".document",
|
|
23
23
|
".vimproject",
|
|
24
24
|
"LICENSE.txt",
|
|
25
|
-
"README.
|
|
25
|
+
"README.md",
|
|
26
26
|
"Rakefile",
|
|
27
27
|
"VERSION",
|
|
28
|
+
"doc/Python.md",
|
|
28
29
|
"lib/scout-rig.rb",
|
|
29
30
|
"lib/scout/python.rb",
|
|
30
31
|
"lib/scout/python/paths.rb",
|
|
31
32
|
"lib/scout/python/run.rb",
|
|
32
33
|
"lib/scout/python/script.rb",
|
|
33
34
|
"lib/scout/python/util.rb",
|
|
35
|
+
"lib/scout/workflow/python.rb",
|
|
36
|
+
"lib/scout/workflow/python/inputs.rb",
|
|
37
|
+
"lib/scout/workflow/python/task.rb",
|
|
34
38
|
"python/scout/__init__.py",
|
|
35
|
-
"python/scout/
|
|
36
|
-
"python/scout/__pycache__/workflow.cpython-310.pyc",
|
|
39
|
+
"python/scout/runner.py",
|
|
37
40
|
"python/scout/workflow.py",
|
|
38
41
|
"python/scout/workflow/remote.py",
|
|
39
42
|
"python/test.py",
|
|
40
43
|
"scout-rig.gemspec",
|
|
44
|
+
"test/scout/python/test_run.rb",
|
|
41
45
|
"test/scout/python/test_script.rb",
|
|
42
46
|
"test/scout/python/test_util.rb",
|
|
43
47
|
"test/scout/test_python.rb",
|
|
48
|
+
"test/scout/workflow/python/test_task.rb",
|
|
49
|
+
"test/scout/workflow/test_python.rb",
|
|
44
50
|
"test/test_helper.rb"
|
|
45
51
|
]
|
|
46
52
|
s.homepage = "http://github.com/mikisvaz/scout-rig".freeze
|
|
47
53
|
s.licenses = ["MIT".freeze]
|
|
48
|
-
s.rubygems_version = "3.
|
|
54
|
+
s.rubygems_version = "3.7.0.dev".freeze
|
|
49
55
|
s.summary = "Scouts rigging things together".freeze
|
|
50
56
|
|
|
51
57
|
s.specification_version = 4
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
|
|
2
|
+
require 'scout/python'
|
|
3
|
+
require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1')
|
|
4
|
+
|
|
5
|
+
class TestClass < Test::Unit::TestCase
|
|
6
|
+
def __test_run_threaded
|
|
7
|
+
|
|
8
|
+
ScoutPython.run_threaded :sys do
|
|
9
|
+
paths = sys.path()
|
|
10
|
+
puts paths
|
|
11
|
+
end
|
|
12
|
+
ScoutPython.stop_thread
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
|
|
2
|
+
require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1')
|
|
3
|
+
|
|
4
|
+
class TestPythonWorkflowTask < Test::Unit::TestCase
|
|
5
|
+
def test_load_metadata
|
|
6
|
+
code =<<-EOF
|
|
7
|
+
import scout
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
def hello(name: str, excited: bool = False) -> str:
|
|
11
|
+
"""
|
|
12
|
+
Greet a user.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
name : str
|
|
17
|
+
The name of the person to greet.
|
|
18
|
+
excited : bool, optional
|
|
19
|
+
Whether to add an exclamation mark, by default False.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
str
|
|
24
|
+
A greeting message.
|
|
25
|
+
"""
|
|
26
|
+
return f"Hello, {name}{'!' if excited else ''}"
|
|
27
|
+
|
|
28
|
+
scout.task(hello)
|
|
29
|
+
EOF
|
|
30
|
+
|
|
31
|
+
TmpFile.with_file code do |task_file|
|
|
32
|
+
res = PythonWorkflow.read_python_metadata task_file
|
|
33
|
+
assert_equal 'hello', res.first['name']
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|