scout-rig 0.1.1 → 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 +17 -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/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
data/doc/Python.md
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
# Python (ScoutPython)
|
|
2
|
+
|
|
3
|
+
ScoutPython is the bridge between Ruby (Scout) and Python. It provides:
|
|
4
|
+
|
|
5
|
+
- A thin, ergonomic layer on top of PyCall to execute Python code from Ruby (direct or in a dedicated thread).
|
|
6
|
+
- Import helpers and a safe “binding” scope that keeps Python names local to a call site.
|
|
7
|
+
- A small scripting facility to run ad‑hoc Python text, pass Ruby variables (including TSV) into Python, and return results.
|
|
8
|
+
- Data conversion helpers for numpy/list and pandas DataFrame to/from TSV.
|
|
9
|
+
- Path management to expose package Python modules to sys.path.
|
|
10
|
+
- A companion Python package (scout) with utilities for TSV IO and Workflow execution from Python, and a small remote Workflow client.
|
|
11
|
+
|
|
12
|
+
Sections:
|
|
13
|
+
- Requirements
|
|
14
|
+
- Path management
|
|
15
|
+
- Running Python code from Ruby
|
|
16
|
+
- Binding scopes and imports
|
|
17
|
+
- Scripting: run ad‑hoc Python text
|
|
18
|
+
- Iteration utilities
|
|
19
|
+
- Data conversion and pandas helpers
|
|
20
|
+
- Python-side helper package (scout)
|
|
21
|
+
- Workflow wrapper (local)
|
|
22
|
+
- Remote workflows over HTTP
|
|
23
|
+
- Error handling, logging, and threading
|
|
24
|
+
- API quick reference
|
|
25
|
+
- Examples
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Ruby gem: pycall (PyCall).
|
|
32
|
+
- Python 3 with:
|
|
33
|
+
- pandas (for DataFrame helpers),
|
|
34
|
+
- numpy (for numeric conversions),
|
|
35
|
+
- Ruby gems used in certain paths: python/pickle (for reading pickle files back into Ruby), json.
|
|
36
|
+
|
|
37
|
+
Make sure a Python3 interpreter is available in PATH.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Path management
|
|
42
|
+
|
|
43
|
+
ScoutPython manages Python import paths and pre-populates them with known package Python dirs.
|
|
44
|
+
|
|
45
|
+
- Sources of Python paths:
|
|
46
|
+
- Scout.python.find_all returns Python directories registered via the Path subsystem; these are added automatically at load time.
|
|
47
|
+
- You can add more at runtime.
|
|
48
|
+
|
|
49
|
+
API:
|
|
50
|
+
```ruby
|
|
51
|
+
# Add one or many paths for Python imports
|
|
52
|
+
ScoutPython.add_path("/path/to/python/package")
|
|
53
|
+
ScoutPython.add_paths(%w[/opt/mypkg/py /usr/local/mytools/py])
|
|
54
|
+
|
|
55
|
+
# Apply registered paths to sys.path (idempotent; called on init and before run_simple)
|
|
56
|
+
ScoutPython.process_paths
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
process_paths executes in a Python context:
|
|
60
|
+
```ruby
|
|
61
|
+
ScoutPython.run_direct 'sys' do
|
|
62
|
+
ScoutPython.paths.each { |p| sys.path.append p }
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Running Python code from Ruby
|
|
69
|
+
|
|
70
|
+
ScoutPython wraps PyCall to simplify imports and execution. The central methods accept optional import directives and a block of Python-interacting Ruby.
|
|
71
|
+
|
|
72
|
+
Imports argument forms:
|
|
73
|
+
- String/Symbol module only: pyimport "numpy"
|
|
74
|
+
- Array of names to import from module: pyfrom "module.submodule", import: [:Class, :func]
|
|
75
|
+
- Hash passed to pyimport (e.g., aliases): pyimport "numpy", as: :np
|
|
76
|
+
|
|
77
|
+
Methods:
|
|
78
|
+
|
|
79
|
+
- run(mod = nil, imports = nil) { ... }
|
|
80
|
+
- Initialize PyCall once (init_scout), ensure paths, run block. Garbage collects after run.
|
|
81
|
+
- run_simple(mod = nil, imports = nil) { ... }
|
|
82
|
+
- Same as run but without init_scout and extra GC. Synchronizes and calls process_paths.
|
|
83
|
+
- run_direct(mod = nil, imports = nil) { ... }
|
|
84
|
+
- Run without synchronization; perform a single pyimport/pyfrom (if provided) and module_eval the block.
|
|
85
|
+
- run_threaded(mod = nil, imports = nil) { ... }
|
|
86
|
+
- Execute in a dedicated background thread (see Threading below). Optional import executed in thread context.
|
|
87
|
+
- run_log(mod = nil, imports = nil, severity = 0, severity_err = nil) { ... }
|
|
88
|
+
- Wrap the execution with Log.trap_std to capture/route Python stdout/stderr.
|
|
89
|
+
- run_log_stderr(mod = nil, imports = nil, severity = 0) { ... }
|
|
90
|
+
- Capture only Python stderr via Log.trap_stderr.
|
|
91
|
+
- stop_thread
|
|
92
|
+
- Stop the background Python thread, join/kill, GC, and PyCall.finalize (if available).
|
|
93
|
+
|
|
94
|
+
Examples:
|
|
95
|
+
```ruby
|
|
96
|
+
# Simple import and call
|
|
97
|
+
ScoutPython.run 'numpy', as: :np do
|
|
98
|
+
a = np.array([1,2,3])
|
|
99
|
+
a.sum # => Py object; can call PyCall on it
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# From submodule and alias
|
|
103
|
+
ScoutPython.run "tensorflow.keras.models", import: :Sequential do
|
|
104
|
+
defined?(self::Sequential) # => true in this scope
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Threaded execution
|
|
108
|
+
arr = ScoutPython.run_threaded :numpy, as: :np do
|
|
109
|
+
np.array([1,2])
|
|
110
|
+
end
|
|
111
|
+
ScoutPython.stop_thread
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Binding scopes and imports
|
|
117
|
+
|
|
118
|
+
Use a dedicated Binding (includes PyCall::Import) to localize imported names so they don’t leak into other scopes.
|
|
119
|
+
|
|
120
|
+
- new_binding → returns a Binding instance with PyCall::Import mixed in.
|
|
121
|
+
- binding_run(binding = nil, *args) { ... }
|
|
122
|
+
- Create a new Binding, instance_exec the block, then discard.
|
|
123
|
+
|
|
124
|
+
Example (from tests):
|
|
125
|
+
```ruby
|
|
126
|
+
raised = false
|
|
127
|
+
ScoutPython.binding_run do
|
|
128
|
+
pyimport :torch
|
|
129
|
+
pyfrom :torch, import: ["nn"]
|
|
130
|
+
begin
|
|
131
|
+
torch
|
|
132
|
+
rescue
|
|
133
|
+
raised = true
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
raised # => false (torch available inside)
|
|
137
|
+
|
|
138
|
+
raised = false
|
|
139
|
+
ScoutPython.binding_run do
|
|
140
|
+
begin
|
|
141
|
+
torch # not defined in this fresh binding
|
|
142
|
+
rescue
|
|
143
|
+
raised = true
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
raised # => true
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Import helpers:
|
|
150
|
+
- import_method(module_name, method_name, as=nil) → Method
|
|
151
|
+
- call_method(module_name, method_name, *args)
|
|
152
|
+
- get_module(module_name) → imported module object (aliased safely as name with dots replaced by underscores)
|
|
153
|
+
- get_class(module_name, class_name)
|
|
154
|
+
- class_new_obj(module_name, class_name, args={}) → instantiate class with keyword args
|
|
155
|
+
- exec(script) → PyCall.exec(script) one-liner
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Scripting: run ad‑hoc Python text
|
|
160
|
+
|
|
161
|
+
script(text, variables = {}) runs a Python script in a subprocess and returns the value of the Python variable result. It:
|
|
162
|
+
|
|
163
|
+
- Serializes provided variables into Python assignments at the top of the script:
|
|
164
|
+
- Ruby primitives (String, Numeric, true/false/nil) mapped to Python literals.
|
|
165
|
+
- Arrays and Hashes transformed recursively.
|
|
166
|
+
- TSV values are written to a temporary file and loaded using the companion Python function scout.tsv(file), yielding a pandas DataFrame.
|
|
167
|
+
- Appends a “save result” snippet to persist the result to a temporary file (default: Pickle).
|
|
168
|
+
- Executes the script with python3 via CMD.cmd_log, wiring PYTHONPATH from ScoutPython.paths.
|
|
169
|
+
- Loads the result back (default: pickled result via python/pickle gem) and returns it.
|
|
170
|
+
|
|
171
|
+
By default, pickle is used:
|
|
172
|
+
- save_script_result_pickle(file) → Python code to pickle.dump(result, file)
|
|
173
|
+
- load_pickle(file) → Ruby loads via Python::Pickle
|
|
174
|
+
|
|
175
|
+
Alternatively, you can alias to JSON-based persistence:
|
|
176
|
+
- save_script_result_json(file)
|
|
177
|
+
- load_json(file)
|
|
178
|
+
- Overwrite aliases if desired:
|
|
179
|
+
```ruby
|
|
180
|
+
class << ScoutPython
|
|
181
|
+
alias save_script_result save_script_result_json
|
|
182
|
+
alias load_result load_json
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Examples (from tests):
|
|
187
|
+
```ruby
|
|
188
|
+
# Simple arithmetic
|
|
189
|
+
res = ScoutPython.script <<~PY, value: 2
|
|
190
|
+
result = value * 3
|
|
191
|
+
PY
|
|
192
|
+
res # => 6
|
|
193
|
+
|
|
194
|
+
# Using pandas via the 'scout' Python helper
|
|
195
|
+
tsv = TSV.setup({}, "Key~ValueA,ValueB#:type=:list")
|
|
196
|
+
tsv["k1"] = %w[a1 b1]; tsv["k2"] = %w[a2 b2]
|
|
197
|
+
|
|
198
|
+
TmpFile.with_file(tsv.to_s) do |tsv_file|
|
|
199
|
+
TmpFile.with_file do |target|
|
|
200
|
+
res = ScoutPython.script <<~PY, file: tsv_file, target: target
|
|
201
|
+
import scout
|
|
202
|
+
df = scout.tsv(file)
|
|
203
|
+
result = df.loc["k2", "ValueB"]
|
|
204
|
+
scout.save_tsv(target, df)
|
|
205
|
+
PY
|
|
206
|
+
res # => "b2"
|
|
207
|
+
TSV.open(target, type: :list)["k2"]["ValueB"] # => "b2"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Pass a TSV directly; script receives a pandas DataFrame
|
|
212
|
+
res = ScoutPython.script <<~PY, df: tsv, target: target
|
|
213
|
+
result = df.loc["k2", "ValueB"]
|
|
214
|
+
scout.save_tsv(target, df)
|
|
215
|
+
PY
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Errors:
|
|
219
|
+
- If the Python subprocess fails (syntax error, exception), script raises ConcurrentStreamProcessFailed (via CMD and ConcurrentStream), with stderr logged if logging enabled.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Iteration utilities
|
|
224
|
+
|
|
225
|
+
Helpers to traverse Python iterables from Ruby, with optional progress bars:
|
|
226
|
+
|
|
227
|
+
- iterate(iterator, bar: nil|true|String) { |elem| ... } → nil
|
|
228
|
+
- Accepts PyCall iterable objects (with __iter__/__next__) or indexable sequences.
|
|
229
|
+
- bar: true creates a default bar; String sets desc.
|
|
230
|
+
- iterate_index(sequence, bar: ...) { |elem| ... } → nil
|
|
231
|
+
- Index-based loop using len and [i].
|
|
232
|
+
- collect(iterator, bar: ...) { |elem| block.call(elem) } → Array
|
|
233
|
+
- Convenience to map Python iterables to Ruby arrays.
|
|
234
|
+
|
|
235
|
+
StopIteration is respected and terminates traversal. Errors mark the bar as error and re-raise.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Data conversion and pandas helpers
|
|
240
|
+
|
|
241
|
+
Low-level converters:
|
|
242
|
+
- py2ruby_a(listlike) / to_a → convert PyCall::List-like into a Ruby Array.
|
|
243
|
+
- list2ruby(list) → deep-convert nested Py lists to Ruby arrays.
|
|
244
|
+
- numpy2ruby(numpy_array) → numpy.ndarray.tolist to Ruby arrays.
|
|
245
|
+
- obj2hash(py_mapping) → build a Ruby Hash by iterating keys and indexing values.
|
|
246
|
+
|
|
247
|
+
pandas ↔ TSV:
|
|
248
|
+
- tsv2df(tsv) → pandas.DataFrame with:
|
|
249
|
+
- index: tsv.keys, columns: tsv.fields, df.columns.name = tsv.key_field.
|
|
250
|
+
- df2tsv(df, options = {})
|
|
251
|
+
- Default options[:type] = :list
|
|
252
|
+
- Builds a TSV with key_field = df.columns.name (or provided), fields from df.columns, and values from df rows.
|
|
253
|
+
|
|
254
|
+
Example (from tests):
|
|
255
|
+
```ruby
|
|
256
|
+
tsv = TSV.setup([], key_field: "Key", fields: %w(Value1 Value2), type: :list)
|
|
257
|
+
tsv["k1"] = %w(V1_1 V2_1)
|
|
258
|
+
tsv["k2"] = %w(V1_2 V2_2)
|
|
259
|
+
|
|
260
|
+
df = ScoutPython.tsv2df(tsv)
|
|
261
|
+
new_tsv = ScoutPython.df2tsv(df)
|
|
262
|
+
new_tsv == tsv # => true
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Python-side helper package (scout)
|
|
268
|
+
|
|
269
|
+
The repository also ships a small Python package named scout (python/scout), intended to be importable from Python code run by ScoutPython or independently.
|
|
270
|
+
|
|
271
|
+
Top-level utilities (scout/__init__.py):
|
|
272
|
+
- cmd(ruby_string=nil) → execute Ruby via rbbt_exec.rb; returns stdout.
|
|
273
|
+
- libdir() → resolve Ruby lib directory via cmd.
|
|
274
|
+
- add_libdir() → prepend libdir/python to sys.path.
|
|
275
|
+
- path(subdir=None, base_dir=None) → convenience location helper ("base" uses ~/.rbbt, "lib" uses libdir).
|
|
276
|
+
- read(subdir, base_dir=None) → read a file via path.
|
|
277
|
+
- inspect(obj) / rich(obj) → inspection helpers (rich requires rich).
|
|
278
|
+
- tsv_header/tsv_preamble/tsv_pandas(tsv_path, ...) → read TSV respecting Scout headers (:sep, :type, headers).
|
|
279
|
+
- tsv(*args, **kwargs) → alias to tsv_pandas.
|
|
280
|
+
- save_tsv(filename, df, key=None) → write pandas DataFrame to TSV with header and key (index_label).
|
|
281
|
+
- save_job_inputs(data: dict) → materialize Python values into files for Workflow inputs.
|
|
282
|
+
- run_job(workflow, task, name='Default', fork=False, clean=False, **kwargs) → shell out to CLI (`rbbt workflow task`) to execute or fork a job and return path or value.
|
|
283
|
+
|
|
284
|
+
Notes:
|
|
285
|
+
- The CLI used in run_job is named rbbt in this package; in Scout-based installs, a compatible executable should be available (often named scout). Setup a symlink or adjust if needed.
|
|
286
|
+
|
|
287
|
+
### Workflow wrapper (local)
|
|
288
|
+
|
|
289
|
+
Pythonic interface to run workflows and get results (python/scout/workflow.py):
|
|
290
|
+
|
|
291
|
+
- Workflow(name)
|
|
292
|
+
- tasks() → list of task names.
|
|
293
|
+
- task_info(name) → JSON string from Workflow.task_info.
|
|
294
|
+
- run(task, **kwargs) → execute and return stdout (uses run_job).
|
|
295
|
+
- fork(task, **kwargs) → submit with fork=True, return a Step(path).
|
|
296
|
+
|
|
297
|
+
- Step(path)
|
|
298
|
+
- info() → job info (JSON parsed via Ruby Step.load(path).info).
|
|
299
|
+
- status(), done(), error(), aborted()
|
|
300
|
+
- join() → poll until completion.
|
|
301
|
+
- load() → load job result via Ruby Step.load(path).load.
|
|
302
|
+
|
|
303
|
+
Example (python/test.py):
|
|
304
|
+
```python
|
|
305
|
+
if __name__ == "__main__":
|
|
306
|
+
import sys
|
|
307
|
+
sys.path.append('python')
|
|
308
|
+
import scout
|
|
309
|
+
import scout.workflow
|
|
310
|
+
wf = scout.workflow.Workflow('Baking')
|
|
311
|
+
step = wf.fork('bake_muffin_tray', add_blueberries=True, clean='recursive')
|
|
312
|
+
step.join()
|
|
313
|
+
print(step.load())
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Remote workflows over HTTP
|
|
317
|
+
|
|
318
|
+
A minimal client for remote Workflow services (python/scout/workflow/remote.py):
|
|
319
|
+
|
|
320
|
+
- RemoteWorkflow(url)
|
|
321
|
+
- init_remote_tasks() → populate available tasks.
|
|
322
|
+
- task_info(name) → JSON
|
|
323
|
+
- job(task, **kwargs) → start a job, returns RemoteStep.
|
|
324
|
+
|
|
325
|
+
- RemoteStep(url)
|
|
326
|
+
- info(), status()
|
|
327
|
+
- done(), error(), running()
|
|
328
|
+
- wait(time=1)
|
|
329
|
+
- raw() → GET bytes of result
|
|
330
|
+
- json() → GET JSON result
|
|
331
|
+
|
|
332
|
+
Requests are made via requests; results are obtained by GET/POST using a _format parameter (raw/json).
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Error handling, logging, and threading
|
|
337
|
+
|
|
338
|
+
- Logging:
|
|
339
|
+
- run_log and run_log_stderr wrap execution in Log.trap_std/stderr; stdout/stderr lines are routed to the Scout Log with chosen severity.
|
|
340
|
+
- script:
|
|
341
|
+
- Uses CMD.cmd_log to launch python3; stderr is logged; non-zero exit raises ConcurrentStreamProcessFailed on join (see CMD and ConcurrentStream docs).
|
|
342
|
+
- Threaded execution:
|
|
343
|
+
- A dedicated thread processes queued blocks (Queue IN/OUT).
|
|
344
|
+
- stop_thread sends a :stop sentinel, joins or kills the thread, triggers GC, and calls PyCall.finalize if available.
|
|
345
|
+
- At exit, ScoutPython attempts to stop non-main threads, run GC while Python is still initialized, and touch PyCall.builtins.object to validate GIL access.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## API quick reference
|
|
350
|
+
|
|
351
|
+
Path management:
|
|
352
|
+
- ScoutPython.paths → Array of Python paths
|
|
353
|
+
- ScoutPython.add_path(path)
|
|
354
|
+
- ScoutPython.add_paths(paths)
|
|
355
|
+
- ScoutPython.process_paths
|
|
356
|
+
|
|
357
|
+
Execution:
|
|
358
|
+
- ScoutPython.run(mod=nil, imports=nil) { ... }
|
|
359
|
+
- ScoutPython.run_simple(mod=nil, imports=nil) { ... }
|
|
360
|
+
- ScoutPython.run_direct(mod=nil, imports=nil) { ... }
|
|
361
|
+
- ScoutPython.run_threaded(mod=nil, imports=nil) { ... }
|
|
362
|
+
- ScoutPython.stop_thread
|
|
363
|
+
- ScoutPython.run_log(mod=nil, imports=nil, severity=0, severity_err=nil) { ... }
|
|
364
|
+
- ScoutPython.run_log_stderr(mod=nil, imports=nil, severity=0) { ... }
|
|
365
|
+
|
|
366
|
+
Binding/import helpers:
|
|
367
|
+
- ScoutPython.new_binding
|
|
368
|
+
- ScoutPython.binding_run(binding=nil, *args) { ... }
|
|
369
|
+
- ScoutPython.import_method(module_name, method_name, as=nil)
|
|
370
|
+
- ScoutPython.call_method(module_name, method_name, *args)
|
|
371
|
+
- ScoutPython.get_module(module_name)
|
|
372
|
+
- ScoutPython.get_class(module_name, class_name)
|
|
373
|
+
- ScoutPython.class_new_obj(module_name, class_name, args={})
|
|
374
|
+
- ScoutPython.exec(script)
|
|
375
|
+
|
|
376
|
+
Iteration:
|
|
377
|
+
- ScoutPython.iterate(iterator, bar: nil|true|String) { |elem| ... }
|
|
378
|
+
- ScoutPython.iterate_index(sequence, bar: ...) { |elem| ... }
|
|
379
|
+
- ScoutPython.collect(iterator, bar: ...) { |elem| ... } → Array
|
|
380
|
+
|
|
381
|
+
Scripting:
|
|
382
|
+
- ScoutPython.script(text, variables={}) → result
|
|
383
|
+
- ScoutPython.save_script_result_pickle(file) / load_pickle(file)
|
|
384
|
+
- ScoutPython.save_script_result_json(file) / load_json(file)
|
|
385
|
+
- Aliases (defaults): save_script_result → pickle; load_result → load_pickle
|
|
386
|
+
|
|
387
|
+
Data conversion:
|
|
388
|
+
- ScoutPython.py2ruby_a(obj) / ScoutPython.to_a(obj)
|
|
389
|
+
- ScoutPython.list2ruby(list)
|
|
390
|
+
- ScoutPython.numpy2ruby(numpy_array)
|
|
391
|
+
- ScoutPython.obj2hash(py_mapping)
|
|
392
|
+
- ScoutPython.tsv2df(tsv) → pandas.DataFrame
|
|
393
|
+
- ScoutPython.df2tsv(df, options={}) → TSV
|
|
394
|
+
|
|
395
|
+
Python package (scout):
|
|
396
|
+
- scout.tsv(path, ...) → pandas DataFrame (header-aware)
|
|
397
|
+
- scout.save_tsv(path, df, key=None)
|
|
398
|
+
- scout.run_job(workflow, task, name='Default', fork=False, clean=False, **inputs) → job path or stdout
|
|
399
|
+
- scout.workflow.Workflow(name).run/fork/tasks/task_info
|
|
400
|
+
- scout.workflow.Step(path).info/status/join/load
|
|
401
|
+
- scout.workflow.remote.RemoteWorkflow/RemoteStep for HTTP services
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Examples
|
|
406
|
+
|
|
407
|
+
Direct PyCall use with imports:
|
|
408
|
+
```ruby
|
|
409
|
+
# Print sys.path in a background Python thread
|
|
410
|
+
ScoutPython.run_threaded :sys do
|
|
411
|
+
paths = sys.path()
|
|
412
|
+
puts paths
|
|
413
|
+
end
|
|
414
|
+
ScoutPython.stop_thread
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Script with result:
|
|
418
|
+
```ruby
|
|
419
|
+
res = ScoutPython.script <<~PY, value: 2
|
|
420
|
+
result = value * 3
|
|
421
|
+
PY
|
|
422
|
+
# => 6
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Script with TSV and pandas:
|
|
426
|
+
```ruby
|
|
427
|
+
tsv = TSV.setup({}, "Key~ValueA,ValueB#:type=:list")
|
|
428
|
+
tsv["k1"] = ["a1", "b1"]; tsv["k2"] = ["a2", "b2"]
|
|
429
|
+
|
|
430
|
+
TmpFile.with_file do |target|
|
|
431
|
+
res = ScoutPython.script <<~PY, df: tsv, target: target
|
|
432
|
+
result = df.loc["k2", "ValueB"]
|
|
433
|
+
scout.save_tsv(target, df)
|
|
434
|
+
PY
|
|
435
|
+
res # => "b2"
|
|
436
|
+
TSV.open(target, type: :list)["k2"]["ValueB"] # => "b2"
|
|
437
|
+
end
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Numpy conversion:
|
|
441
|
+
```ruby
|
|
442
|
+
ra = ScoutPython.run :numpy, as: :np do
|
|
443
|
+
na = np.array([[[1,2,3], [4,5,6]]])
|
|
444
|
+
ScoutPython.numpy2ruby(na)
|
|
445
|
+
end
|
|
446
|
+
ra[0][1][2] # => 6
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
pandas ↔ TSV:
|
|
450
|
+
```ruby
|
|
451
|
+
tsv = TSV.setup([], key_field: "Key", fields: %w(Value1 Value2), type: :list)
|
|
452
|
+
tsv["k1"] = %w(V1_1 V2_1)
|
|
453
|
+
tsv["k2"] = %w(V1_2 V2_2)
|
|
454
|
+
|
|
455
|
+
df = ScoutPython.tsv2df(tsv)
|
|
456
|
+
new_tsv = ScoutPython.df2tsv(df)
|
|
457
|
+
new_tsv == tsv # => true
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Binding-local imports:
|
|
461
|
+
```ruby
|
|
462
|
+
ScoutPython.binding_run do
|
|
463
|
+
pyimport :torch
|
|
464
|
+
# torch is available here
|
|
465
|
+
end
|
|
466
|
+
# torch is not defined here
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Python-side Workflow usage:
|
|
470
|
+
```python
|
|
471
|
+
import scout.workflow as sw
|
|
472
|
+
|
|
473
|
+
wf = sw.Workflow('Baking')
|
|
474
|
+
print(wf.tasks())
|
|
475
|
+
step = wf.fork('bake_muffin_tray', add_blueberries=True, clean='recursive')
|
|
476
|
+
step.join()
|
|
477
|
+
print(step.load())
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
ScoutPython gives you a compact, production-friendly toolbox to interoperate with Python: safe imports, threaded or synchronous execution, TSV/pandas integration, and Workflow orchestration from both Ruby and Python. Use run/script for quick integrations, the conversion helpers to pass data efficiently, and the Python scout package to drive Scout Workflows from Python environments.
|
data/lib/scout/python/run.rb
CHANGED
|
@@ -31,6 +31,7 @@ module ScoutPython
|
|
|
31
31
|
|
|
32
32
|
self.thread ||= Thread.new do
|
|
33
33
|
require 'pycall'
|
|
34
|
+
ScoutPython.init_scout
|
|
34
35
|
ScoutPython.process_paths
|
|
35
36
|
begin
|
|
36
37
|
while block = QUEUE_IN.pop
|
|
@@ -48,8 +49,6 @@ module ScoutPython
|
|
|
48
49
|
rescue Exception
|
|
49
50
|
Log.exception $!
|
|
50
51
|
raise $!
|
|
51
|
-
ensure
|
|
52
|
-
PyCall.finalize if PyCall.respond_to?(:finalize)
|
|
53
52
|
end
|
|
54
53
|
end
|
|
55
54
|
end
|
|
@@ -64,9 +63,14 @@ module ScoutPython
|
|
|
64
63
|
|
|
65
64
|
def self.stop_thread
|
|
66
65
|
self.synchronize do
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
if self.thread && self.thread.alive?
|
|
67
|
+
QUEUE_IN.push :stop
|
|
68
|
+
self.thread.join(2) || self.thread.kill
|
|
69
|
+
GC.start
|
|
70
|
+
PyCall.finalize if PyCall.respond_to?(:finalize)
|
|
71
|
+
end
|
|
72
|
+
self.thread = nil
|
|
73
|
+
end
|
|
70
74
|
end
|
|
71
75
|
|
|
72
76
|
def self.run_direct(mod = nil, imports = nil, &block)
|
|
@@ -104,8 +108,13 @@ module ScoutPython
|
|
|
104
108
|
end
|
|
105
109
|
end
|
|
106
110
|
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
def self.run(...)
|
|
112
|
+
begin
|
|
113
|
+
ScoutPython.init_scout
|
|
114
|
+
run_simple(...)
|
|
115
|
+
ensure
|
|
116
|
+
GC.start
|
|
117
|
+
end
|
|
109
118
|
end
|
|
110
119
|
|
|
111
120
|
def self.run_log(mod = nil, imports = nil, severity = 0, severity_err = nil, &block)
|
data/lib/scout/python/script.rb
CHANGED
data/lib/scout/python.rb
CHANGED
|
@@ -12,12 +12,28 @@ module ScoutPython
|
|
|
12
12
|
|
|
13
13
|
def self.init_scout
|
|
14
14
|
if ! defined?(@@__init_scout_python) || ! @@__init_scout_python
|
|
15
|
+
PyCall.init
|
|
16
|
+
|
|
15
17
|
ScoutPython.process_paths
|
|
16
|
-
res = ScoutPython.
|
|
18
|
+
res = ScoutPython.run_direct do
|
|
17
19
|
Log.debug "Loading python 'scout' module into pycall ScoutPython module"
|
|
18
20
|
pyimport("scout")
|
|
19
21
|
end
|
|
20
22
|
@@__init_scout_python = true
|
|
23
|
+
|
|
24
|
+
at_exit do
|
|
25
|
+
(Thread.list - [Thread.current]).each { |t| t.kill }
|
|
26
|
+
(Thread.list - [Thread.current]).each { |t| t.join rescue nil }
|
|
27
|
+
|
|
28
|
+
# GC while Python is still initialized so PyCall can safely acquire the GIL
|
|
29
|
+
GC.start
|
|
30
|
+
|
|
31
|
+
# (Optional) tiny no-op to ensure GIL path is healthy
|
|
32
|
+
begin
|
|
33
|
+
PyCall.builtins.object
|
|
34
|
+
rescue => _
|
|
35
|
+
end
|
|
36
|
+
end
|
|
21
37
|
end
|
|
22
38
|
end
|
|
23
39
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module PythonWorkflow
|
|
2
|
+
def self.build_python_argv(py_params, values)
|
|
3
|
+
argv = []
|
|
4
|
+
py_params.each do |p|
|
|
5
|
+
name = p['name']
|
|
6
|
+
ptype = p['type']
|
|
7
|
+
default = p['default']
|
|
8
|
+
required = p['required']
|
|
9
|
+
val = values[name]
|
|
10
|
+
|
|
11
|
+
if val.nil?
|
|
12
|
+
next unless required
|
|
13
|
+
next
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
flag = "--#{name}"
|
|
17
|
+
if ptype.start_with?('list[')
|
|
18
|
+
# Accept several input formats for lists:
|
|
19
|
+
# - Ruby Array
|
|
20
|
+
# - Comma-separated string: "a,b,c"
|
|
21
|
+
# - Path to a file: "file.txt" -> read lines
|
|
22
|
+
items = []
|
|
23
|
+
if val.is_a?(String)
|
|
24
|
+
# If file exists, read lines
|
|
25
|
+
if File.exist?(val)
|
|
26
|
+
items = File.readlines(val, chomp: true)
|
|
27
|
+
elsif val.include?(',')
|
|
28
|
+
items = val.split(',').map(&:strip)
|
|
29
|
+
else
|
|
30
|
+
items = [val]
|
|
31
|
+
end
|
|
32
|
+
elsif val.respond_to?(:to_ary)
|
|
33
|
+
items = Array(val).map(&:to_s)
|
|
34
|
+
else
|
|
35
|
+
items = [val.to_s]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# pass flag once followed by all items (argparse with nargs='+' expects this)
|
|
39
|
+
argv << flag
|
|
40
|
+
items.each do |x|
|
|
41
|
+
argv << x.to_s
|
|
42
|
+
end
|
|
43
|
+
elsif ptype == 'boolean'
|
|
44
|
+
if default == true
|
|
45
|
+
argv << "--no-#{name}" unless val
|
|
46
|
+
else
|
|
47
|
+
argv << flag if val
|
|
48
|
+
end
|
|
49
|
+
else
|
|
50
|
+
# For scalar inputs: if given a file path and the file exists, pass the path
|
|
51
|
+
# as-is (the Python side can decide how to handle it). Keep quoting to
|
|
52
|
+
# preserve spaces when later shell-joining.
|
|
53
|
+
argv << flag
|
|
54
|
+
argv << val.to_s
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
argv
|
|
58
|
+
end
|
|
59
|
+
end
|