Module: PythonWorkflow

Defined in:
lib/scout/workflow/python.rb,
lib/scout/workflow/python/task.rb,
lib/scout/workflow/python/inputs.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#python_task_dirObject

Returns the value of attribute python_task_dir.



9
10
11
# File 'lib/scout/workflow/python.rb', line 9

def python_task_dir
  @python_task_dir
end

Class Method Details

.build_python_argv(py_params, values) ⇒ Object



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/scout/workflow/python/inputs.rb', line 2

def self.build_python_argv(py_params, values)
  argv = []
  py_params.each do |p|
    name = p['name']
    ptype = p['type']
    default = p['default']
    required = p['required']
    val = values[name]

    if val.nil?
      next unless required
      next
    end

    flag = "--#{name}"
    if ptype.start_with?('list[')
      # Accept several input formats for lists:
      # - Ruby Array
      # - Comma-separated string: "a,b,c"
      # - Path to a file: "file.txt" -> read lines
      items = []
      if val.is_a?(String)
        # If file exists, read lines
        if File.exist?(val)
          items = File.readlines(val, chomp: true)
        elsif val.include?(',')
          items = val.split(',').map(&:strip)
        else
          items = [val]
        end
      elsif val.respond_to?(:to_ary)
        items = Array(val).map(&:to_s)
      else
        items = [val.to_s]
      end

      # pass flag once followed by all items (argparse with nargs='+' expects this)
      argv << flag
      items.each do |x|
        argv << x.to_s
      end
    elsif ptype == 'boolean'
      if default == true
        argv << "--no-#{name}" unless val
      else
        argv << flag if val
      end
    else
      # For scalar inputs: if given a file path and the file exists, pass the path
      # as-is (the Python side can decide how to handle it). Keep quoting to
      # preserve spaces when later shell-joining.
      argv << flag
      argv << val.to_s
    end
  end
  argv
end

.map_param(p) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/scout/workflow/python/task.rb', line 29

def self.map_param(p)
  desc = p['help'] || ""
  default = p['default']
  required = p['required'] ? true : false

  ruby_type =
    case p['type']
    when 'string' then :string
    when 'integer' then :integer
    when 'float'   then :float
    when 'boolean' then :boolean
    when 'binary'  then :binary
    when 'path'    then :file
    else
      if p['type'].start_with?('list[')
        subtype = p['type'][5..-2]
        ruby_sub = case subtype
                   when 'path' then :file_array
                   else :array
                   end
        ruby_sub
      else
        :string
      end
    end

  options = {}
  options[:required] = true if required

  { name: p['name'], type: ruby_type, desc: desc, default: default, options: options }
end

.map_returns(py_type) ⇒ Object



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/scout/workflow/python/task.rb', line 11

def self.map_returns(py_type)
  case py_type
  when 'string' then :string
  when 'integer' then :integer
  when 'float' then :float
  when 'boolean' then :boolean
  when 'binary' then :binary
  when 'path' then :string
  when 'list', 'array' then :array
  else
    if py_type.start_with?("list[")
      :array
    else
      :string
    end
  end
end

.read_python_metadata(file) ⇒ Object



5
6
7
8
9
# File 'lib/scout/workflow/python/task.rb', line 5

def self.(file)
  out = ScoutPython.run_file file, '--scout-metadata'
  raise "Error getting metadata from #{file}: #{err}" unless out.exit_status == 0
  JSON.parse(out.read)
end

Instance Method Details

#python_task(task_sym, file: nil, returns: nil, extension: nil, desc: nil) ⇒ Object



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/scout/workflow/python/task.rb', line 61

def python_task(task_sym, file: nil, returns: nil, extension: nil, desc: nil)
  name = task_sym.to_s
  file ||= python_task_dir[name].find_with_extension('py')
  raise "Python task file not found: #{file}" unless File.exist?(file)

  metas = PythonWorkflow.(file)
  metas = [metas] unless Array === metas

  # For each function defined in the python file, register a workflow task
  metas.each do |meta|
    meta['returns'] = returns.to_s if returns
    task_desc = desc || meta['description']

    ruby_returns = PythonWorkflow.map_returns(meta['returns'])
    ruby_inputs  = meta['params'].map { |p| PythonWorkflow.map_param(p) }

    ruby_inputs.each do |inp|
      input(inp[:name].to_sym, inp[:type], inp[:desc], inp[:default], inp[:options] || {})
    end

    self.desc(task_desc) if task_desc && !task_desc.empty?
    self.extension(extension) if extension

    task({ meta['name'].to_sym => ruby_returns }) do |*args|
      arg_names = ruby_inputs.map { |i| i[:name] }
      values = {}
      arg_names.each_with_index { |n,i| values[n] = args[i] }

      argv = PythonWorkflow.build_python_argv(meta['params'], values)
      # prefix with function name so the python script runs the desired function
      full_argv = [meta['name']] + argv

      out = ScoutPython.run_file file, Shellwords.shelljoin(full_argv)
      # out is expected to respond to exit_status and read
      raise "Python task #{meta['name']} failed" unless out.exit_status == 0
      txt = out.read.to_s
      # try JSON
      begin
        next JSON.parse(txt)
      rescue JSON::ParserError
        # not JSON; for list returns, split by newline
        if ruby_returns == :array || ruby_returns == :file_array
          next txt.split("\n").map(&:to_s)
        end
        next txt.strip
      end
    end
  end
end