rbbt-util 5.20.26 → 5.21.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/lib/rbbt/annotations.rb +3 -1
- data/lib/rbbt/monitor.rb +12 -10
- data/lib/rbbt/persist.rb +1 -1
- data/lib/rbbt/rest/client.rb +2 -2
- data/lib/rbbt/rest/client/adaptor.rb +1 -0
- data/lib/rbbt/rest/client/get.rb +1 -33
- data/lib/rbbt/rest/client/run.rb +100 -0
- data/lib/rbbt/rest/client/step.rb +26 -73
- data/lib/rbbt/tsv/util.rb +7 -2
- data/lib/rbbt/util/log/progress/report.rb +1 -0
- data/lib/rbbt/util/misc/concurrent_stream.rb +6 -2
- data/lib/rbbt/util/misc/development.rb +18 -14
- data/lib/rbbt/util/misc/exceptions.rb +16 -7
- data/lib/rbbt/util/misc/inspect.rb +3 -2
- data/lib/rbbt/util/misc/pipes.rb +10 -7
- data/lib/rbbt/util/task/job.rb +6 -6
- data/lib/rbbt/workflow.rb +29 -2
- data/lib/rbbt/workflow/accessor.rb +14 -1
- data/lib/rbbt/workflow/definition.rb +23 -4
- data/lib/rbbt/workflow/soap.rb +1 -0
- data/lib/rbbt/workflow/step/dependencies.rb +5 -3
- data/lib/rbbt/workflow/step/run.rb +23 -5
- data/share/rbbt_commands/workflow/server +16 -3
- data/share/rbbt_commands/workflow/task +9 -0
- data/share/workflow_config.ru +35 -7
- data/test/rbbt/test_workflow.rb +24 -0
- data/test/rbbt/tsv/test_stream.rb +16 -0
- data/test/rbbt/util/misc/test_bgzf.rb +1 -1
- data/test/rbbt/util/test_misc.rb +29 -12
- metadata +64 -63
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 752166bf720d5525c25bf96a2a01915c5ee7430c
|
4
|
+
data.tar.gz: 973d98ccede9449355b494982789b8cb82c325e1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 31ee0806440ed08fd319e7dfe9cbcd524ec7f40bbe10cfa8b56c411172d0e27d84397b8a64674000b6b340f433e79425fa8a779d640a802c3d835cc861e7df49
|
7
|
+
data.tar.gz: 7763048e9ee7786cb9270e9656b821093c5d67e2f2bdef8f1fc3448b11b0fa187fcf1f53a6918de4918a759bfec4ee4755b07313015efb6620895583cb4e9549
|
data/lib/rbbt/annotations.rb
CHANGED
@@ -69,7 +69,9 @@ module Annotated
|
|
69
69
|
def info(masked = false)
|
70
70
|
|
71
71
|
if @info.nil?
|
72
|
-
|
72
|
+
annotation_values = self.annotation_values
|
73
|
+
annotation_values = annotation_values.dup unless annotation_values.nil?
|
74
|
+
info = annotation_values
|
73
75
|
info[:annotation_types] = annotation_types
|
74
76
|
info[:annotated_array] = true if AnnotatedArray === self
|
75
77
|
@info = info
|
data/lib/rbbt/monitor.rb
CHANGED
@@ -10,16 +10,18 @@ module Rbbt
|
|
10
10
|
|
11
11
|
SENSIBLE_WRITE_DIRS = Misc.sensiblewrite_dir.find_all
|
12
12
|
|
13
|
-
PERSIST_DIRS = Rbbt.share.find_all + Rbbt.var.cache.persistence.find_all
|
13
|
+
PERSIST_DIRS = Rbbt.share.find_all + Rbbt.var.cache.persistence.find_all
|
14
14
|
|
15
15
|
JOB_DIRS = Rbbt.var.jobs.find_all
|
16
16
|
|
17
|
+
MUTEX_FOR_THREAD_EXCLUSIVE = Mutex.new
|
18
|
+
|
17
19
|
def self.dump_memory(file, obj = nil)
|
18
20
|
Log.info "Dumping #{obj} objects into #{ file }"
|
19
21
|
Thread.new do
|
20
22
|
while true
|
21
23
|
Open.write(file) do |f|
|
22
|
-
|
24
|
+
MUTEX_FOR_THREAD_EXCLUSIVE.synchronize do
|
23
25
|
GC.start
|
24
26
|
ObjectSpace.each_object(obj) do |o|
|
25
27
|
f.puts "---"
|
@@ -60,9 +62,9 @@ module Rbbt
|
|
60
62
|
lock_info[f] = {}
|
61
63
|
begin
|
62
64
|
lock_info[f].merge!(file_time(f))
|
63
|
-
if File.size(f) > 0
|
65
|
+
if File.size(f) > 0
|
64
66
|
info = Open.open(f) do |s|
|
65
|
-
YAML.load(s)
|
67
|
+
YAML.load(s)
|
66
68
|
end
|
67
69
|
IndiferentHash.setup(info)
|
68
70
|
lock_info[f][:pid] = info[:pid]
|
@@ -98,7 +100,7 @@ module Rbbt
|
|
98
100
|
end
|
99
101
|
|
100
102
|
# PERSISTS
|
101
|
-
|
103
|
+
|
102
104
|
def self.persists(dirs = PERSIST_DIRS)
|
103
105
|
dirs.collect do |dir|
|
104
106
|
next unless Open.exists? dir
|
@@ -145,7 +147,7 @@ module Rbbt
|
|
145
147
|
files = `find "#{ taskdir }/" -not -type d -not -path "*/*.files/*" 2>/dev/null`.split("\n").sort
|
146
148
|
_files = Set.new files
|
147
149
|
TSV.traverse files, :type => :array, :into => jobs do |file|
|
148
|
-
if m = file.match(/(.*).info$/)
|
150
|
+
if m = file.match(/(.*).(info|pid)$/)
|
149
151
|
file = m[1]
|
150
152
|
end
|
151
153
|
|
@@ -161,7 +163,7 @@ module Rbbt
|
|
161
163
|
if _files.include? file
|
162
164
|
info = info.merge(file_time(file))
|
163
165
|
info[:done] = true
|
164
|
-
info[:info_file] = File.
|
166
|
+
info[:info_file] = File.exist?(info_file) ? info_file : nil
|
165
167
|
else
|
166
168
|
info = info.merge({:info_file => info_file, :done => false})
|
167
169
|
end
|
@@ -200,7 +202,7 @@ module Rbbt
|
|
200
202
|
job = f.sub(/\.(info|files)/,'')
|
201
203
|
|
202
204
|
jobs[workflow][task][job] ||= {}
|
203
|
-
if jobs[workflow][task][job][:status].nil?
|
205
|
+
if jobs[workflow][task][job][:status].nil?
|
204
206
|
status = nil
|
205
207
|
status = :done if Open.exists? job
|
206
208
|
if status.nil? and f=~/\.info/
|
@@ -208,7 +210,7 @@ module Rbbt
|
|
208
210
|
Step::INFO_SERIALIAZER.load(Open.read(f, :mode => 'rb'))
|
209
211
|
rescue
|
210
212
|
{}
|
211
|
-
end
|
213
|
+
end
|
212
214
|
status = info[:status]
|
213
215
|
pid = info[:pid]
|
214
216
|
end
|
@@ -224,7 +226,7 @@ module Rbbt
|
|
224
226
|
|
225
227
|
def self.load_lock(lock)
|
226
228
|
begin
|
227
|
-
info = Misc.insist 3 do
|
229
|
+
info = Misc.insist 3 do
|
228
230
|
YAML.load(Open.read(lock))
|
229
231
|
end
|
230
232
|
info.values_at "pid", "ppid", "time"
|
data/lib/rbbt/persist.rb
CHANGED
data/lib/rbbt/rest/client.rb
CHANGED
@@ -11,7 +11,7 @@ require 'rbbt/rest/client/step'
|
|
11
11
|
class WorkflowRESTClient
|
12
12
|
include Workflow
|
13
13
|
|
14
|
-
attr_accessor :url, :name, :exec_exports, :asynchronous_exports, :
|
14
|
+
attr_accessor :url, :name, :exec_exports, :synchronous_exports, :asynchronous_exports, :stream_exports
|
15
15
|
|
16
16
|
def initialize(url, name)
|
17
17
|
Log.debug{ "Loading remote workflow #{ name }: #{ url }" }
|
@@ -45,7 +45,7 @@ class WorkflowRESTClient
|
|
45
45
|
|
46
46
|
|
47
47
|
stream_input = @can_stream ? task_info(task)[:input_options].select{|k,o| o[:stream] }.collect{|k,o| k }.first : nil
|
48
|
-
RemoteStep.new(url, task, name, fixed_inputs, task_info[:result_type], task_info[:result_description], @exec_exports.include?(task), stream_input)
|
48
|
+
RemoteStep.new(url, task, name, fixed_inputs, task_info[:result_type], task_info[:result_description], @exec_exports.include?(task), @stream_exports.include?(task), stream_input)
|
49
49
|
end
|
50
50
|
|
51
51
|
def load_id(id)
|
@@ -61,6 +61,7 @@ class WorkflowRESTClient
|
|
61
61
|
@asynchronous_exports = task_exports["asynchronous"].collect{|task| task.to_sym }
|
62
62
|
@synchronous_exports = task_exports["synchronous"].collect{|task| task.to_sym }
|
63
63
|
@exec_exports = task_exports["exec"].collect{|task| task.to_sym }
|
64
|
+
@stream_exports = task_exports["stream"].collect{|task| task.to_sym }
|
64
65
|
@can_stream = task_exports["can_stream"]
|
65
66
|
end
|
66
67
|
end
|
data/lib/rbbt/rest/client/get.rb
CHANGED
@@ -54,11 +54,11 @@ class WorkflowRESTClient
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def self.get_raw(url, params = {})
|
57
|
-
Log.debug{ "RestClient get_raw: #{ url } - #{Misc.fingerprint params}" }
|
58
57
|
params = params.merge({ :_format => 'raw' })
|
59
58
|
params = fix_params params
|
60
59
|
res = capture_exception do
|
61
60
|
Misc.insist(2, 0.5) do
|
61
|
+
Log.debug{ "RestClient get_raw: #{ url } - #{Misc.fingerprint params}" }
|
62
62
|
res = RestClient.get(URI.encode(url), :params => params)
|
63
63
|
raise TryAgain if res.code == 202
|
64
64
|
res
|
@@ -119,36 +119,4 @@ class WorkflowRESTClient
|
|
119
119
|
end
|
120
120
|
end
|
121
121
|
|
122
|
-
def self.stream_job(task_url, task_params, stream_input, cache_type = :exec)
|
123
|
-
require 'rbbt/util/misc/multipart_payload'
|
124
|
-
WorkflowRESTClient.capture_exception do
|
125
|
-
Log.debug{ "RestClient stream #{Process.pid}: #{ task_url } #{stream_input} #{cache_type} - #{Misc.fingerprint task_params}" }
|
126
|
-
res = RbbtMutiplartPayload.issue task_url, task_params, stream_input, nil, nil, true
|
127
|
-
type = res.gets
|
128
|
-
out = case type.strip
|
129
|
-
when "LOCATION"
|
130
|
-
@url = res.gets
|
131
|
-
@url.sub!(/\?.*/,'')
|
132
|
-
WorkflowRESTClient.get_raw(@url)
|
133
|
-
when /STREAM: (.*)/
|
134
|
-
@url = $1.strip
|
135
|
-
res.callback = Proc.new do
|
136
|
-
Log.medium "Done streaming result from #{@url}"
|
137
|
-
@done = true
|
138
|
-
end
|
139
|
-
res
|
140
|
-
when "BULK"
|
141
|
-
begin
|
142
|
-
res.read
|
143
|
-
ensure
|
144
|
-
@done = true
|
145
|
-
end
|
146
|
-
else
|
147
|
-
raise "What? " + type
|
148
|
-
end
|
149
|
-
ConcurrentStream.setup(out, :filename => @url)
|
150
|
-
out
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
122
|
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
class WorkflowRESTClient::RemoteStep
|
2
|
+
|
3
|
+
def stream_job(task_url, task_params, stream_input, cache_type = :exec)
|
4
|
+
require 'rbbt/util/misc/multipart_payload'
|
5
|
+
WorkflowRESTClient.capture_exception do
|
6
|
+
@streaming = true
|
7
|
+
|
8
|
+
Log.debug{ "RestClient stream #{Process.pid}: #{ task_url } #{stream_input} #{cache_type} - #{Misc.fingerprint task_params}" }
|
9
|
+
res = RbbtMutiplartPayload.issue task_url, task_params, stream_input, nil, nil, true
|
10
|
+
type = res.gets
|
11
|
+
|
12
|
+
out = case type.strip
|
13
|
+
when "LOCATION"
|
14
|
+
@url = res.gets
|
15
|
+
@url.sub!(/\?.*/,'')
|
16
|
+
join
|
17
|
+
WorkflowRESTClient.get_raw(@url)
|
18
|
+
@done = true
|
19
|
+
@streaming = false
|
20
|
+
when /STREAM: (.*)/
|
21
|
+
@url = $1.strip
|
22
|
+
res.callback = Proc.new do
|
23
|
+
Log.medium "Done streaming result from #{@url}"
|
24
|
+
@done = true
|
25
|
+
@streaming = false
|
26
|
+
end
|
27
|
+
res
|
28
|
+
when "BULK"
|
29
|
+
begin
|
30
|
+
res.read
|
31
|
+
ensure
|
32
|
+
@done = true
|
33
|
+
@streaming = false
|
34
|
+
end
|
35
|
+
else
|
36
|
+
raise "What? " + type
|
37
|
+
end
|
38
|
+
|
39
|
+
ConcurrentStream.setup(out, :filename => @url)
|
40
|
+
|
41
|
+
out
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def execute_job(task_url, task_params, cache_type)
|
46
|
+
WorkflowRESTClient.capture_exception do
|
47
|
+
task_url = URI.encode(File.join(base_url, task.to_s))
|
48
|
+
|
49
|
+
sout, sin = Misc.pipe
|
50
|
+
|
51
|
+
post_thread = Thread.new(Thread.current) do |parent|
|
52
|
+
bl = lambda do |rok|
|
53
|
+
if Net::HTTPOK === rok
|
54
|
+
_url = rok["RBBT-STREAMING-JOB-URL"]
|
55
|
+
@url = File.join(task_url, File.basename(_url)) if _url
|
56
|
+
rok.read_body do |c,_a, _b|
|
57
|
+
sin.write c
|
58
|
+
end
|
59
|
+
sin.close
|
60
|
+
else
|
61
|
+
parent.raise "Error in RestClient: " << rok.message
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
Log.debug{ "RestClient execute: #{ url } - #{Misc.fingerprint task_params}" }
|
66
|
+
RestClient::Request.execute(:method => :post, :url => task_url, :payload => task_params, :block_response => bl)
|
67
|
+
end
|
68
|
+
|
69
|
+
reader = Zlib::GzipReader.new(sout)
|
70
|
+
res_io = Misc.open_pipe do |sin|
|
71
|
+
while c = reader.read(Misc::BLOCK_SIZE)
|
72
|
+
sin.write c
|
73
|
+
end
|
74
|
+
sin.close
|
75
|
+
@done = true
|
76
|
+
end
|
77
|
+
|
78
|
+
ConcurrentStream.setup(res_io, :threads => [post_thread]) do
|
79
|
+
@done = true
|
80
|
+
@streaming = false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def _run_job(cache_type = :async)
|
86
|
+
get_streams
|
87
|
+
|
88
|
+
task_url = URI.encode(File.join(base_url, task.to_s))
|
89
|
+
WorkflowRESTClient.__prepare_inputs_for_restclient(inputs)
|
90
|
+
task_params = inputs.merge(:_cache_type => cache_type, :jobname => base_name, :_format => [:string, :boolean, :tsv, :annotations].include?(result_type) ? :raw : :json)
|
91
|
+
|
92
|
+
if cache_type == :stream or cache_type == :exec and stream_input and inputs[stream_input]
|
93
|
+
io = self.stream_job(task_url, task_params, stream_input, cache_type)
|
94
|
+
return io
|
95
|
+
else
|
96
|
+
execute_job(task_url, task_params, cache_type)
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
@@ -18,22 +18,34 @@ class WorkflowRESTClient
|
|
18
18
|
|
19
19
|
class RemoteStep < Step
|
20
20
|
|
21
|
-
attr_accessor :url, :base_url, :task, :base_name, :inputs, :result_type, :result_description, :is_exec, :stream_input
|
21
|
+
attr_accessor :url, :base_url, :task, :base_name, :inputs, :result_type, :result_description, :is_exec, :is_stream, :stream_input
|
22
22
|
|
23
|
-
def initialize(base_url, task = nil, base_name = nil, inputs = nil, result_type = nil, result_description = nil, is_exec = false, stream_input = nil)
|
24
|
-
@base_url, @task, @base_name, @inputs, @result_type, @result_description, @is_exec, @stream_input = base_url, task, base_name, inputs, result_type, result_description, is_exec, stream_input
|
23
|
+
def initialize(base_url, task = nil, base_name = nil, inputs = nil, result_type = nil, result_description = nil, is_exec = false, is_stream = false, stream_input = nil)
|
24
|
+
@base_url, @task, @base_name, @inputs, @result_type, @result_description, @is_exec, @is_stream, @stream_input = base_url, task, base_name, inputs, result_type, result_description, is_exec, is_stream, stream_input
|
25
25
|
@mutex = Mutex.new
|
26
26
|
end
|
27
27
|
|
28
|
-
def run(
|
28
|
+
def run(no_load = false)
|
29
|
+
no_load = @is_stream ? :stream : true if no_load
|
30
|
+
|
29
31
|
@mutex.synchronize do
|
30
32
|
@result ||= begin
|
31
33
|
if @is_exec
|
32
|
-
exec(
|
33
|
-
elsif
|
34
|
+
exec(no_load)
|
35
|
+
elsif no_load == :stream
|
34
36
|
_run_job(:stream)
|
37
|
+
#init_job
|
38
|
+
#join
|
39
|
+
#Misc.open_pipe do |sin|
|
40
|
+
# body = get.body
|
41
|
+
# sin.write body
|
42
|
+
#end
|
43
|
+
elsif no_load
|
44
|
+
init_job
|
45
|
+
nil
|
35
46
|
else
|
36
47
|
init_job
|
48
|
+
join
|
37
49
|
self.load
|
38
50
|
end
|
39
51
|
ensure
|
@@ -41,8 +53,8 @@ class WorkflowRESTClient
|
|
41
53
|
end
|
42
54
|
end
|
43
55
|
|
44
|
-
return @result if
|
45
|
-
|
56
|
+
return @result if no_load == :stream
|
57
|
+
no_load ? path + '?_format=raw' : @result
|
46
58
|
end
|
47
59
|
|
48
60
|
|
@@ -79,7 +91,7 @@ class WorkflowRESTClient
|
|
79
91
|
end
|
80
92
|
|
81
93
|
def abort
|
82
|
-
|
94
|
+
WorkflowRESTClient.get_json(@url + '?_update=abort') if @url and @name
|
83
95
|
end
|
84
96
|
|
85
97
|
def name
|
@@ -99,7 +111,7 @@ class WorkflowRESTClient
|
|
99
111
|
end
|
100
112
|
|
101
113
|
def info(check_lock=false)
|
102
|
-
@done = @info
|
114
|
+
@done = @info && @info[:status] && @info[:status].to_sym == :done
|
103
115
|
@info = Persist.memory("RemoteSteps Info", :url => @url, :persist => !!@done) do
|
104
116
|
init_job unless @url
|
105
117
|
info = WorkflowRESTClient.get_json(File.join(@url, 'info'))
|
@@ -150,6 +162,7 @@ class WorkflowRESTClient
|
|
150
162
|
end
|
151
163
|
|
152
164
|
def grace
|
165
|
+
produce unless @started
|
153
166
|
sleep 0.1 unless started?
|
154
167
|
sleep 0.5 unless started?
|
155
168
|
sleep 1 unless started?
|
@@ -259,72 +272,10 @@ class WorkflowRESTClient
|
|
259
272
|
|
260
273
|
def load
|
261
274
|
params = {}
|
275
|
+
join unless done? or streaming?
|
262
276
|
load_res get
|
263
277
|
end
|
264
278
|
|
265
|
-
def _run_job(cache_type = :async)
|
266
|
-
get_streams
|
267
|
-
if cache_type == :stream or cache_type == :exec and stream_input and inputs[stream_input]
|
268
|
-
task_url = URI.encode(File.join(base_url, task.to_s))
|
269
|
-
WorkflowRESTClient.__prepare_inputs_for_restclient(inputs)
|
270
|
-
task_params = inputs.merge(:_cache_type => cache_type, :jobname => base_name, :_format => [:string, :boolean, :tsv, :annotations].include?(result_type) ? :raw : :json)
|
271
|
-
@streaming = true
|
272
|
-
io = WorkflowRESTClient.stream_job(task_url, task_params, stream_input, cache_type)
|
273
|
-
if IO === io
|
274
|
-
ConcurrentStream.setup(io)
|
275
|
-
io.add_callback do
|
276
|
-
@done = true
|
277
|
-
@streaming = false
|
278
|
-
end
|
279
|
-
else
|
280
|
-
@done = true
|
281
|
-
@streaming = false
|
282
|
-
end
|
283
|
-
|
284
|
-
@url = io.filename if io.filename
|
285
|
-
return io
|
286
|
-
end
|
287
|
-
|
288
|
-
WorkflowRESTClient.capture_exception do
|
289
|
-
@url = URI.encode(File.join(base_url, task.to_s))
|
290
|
-
task_params = inputs.merge(:_cache_type => cache_type, :jobname => base_name, :_format => [:string, :boolean, :tsv, :annotations].include?(result_type) ? :raw : :json)
|
291
|
-
|
292
|
-
sout, sin = Misc.pipe
|
293
|
-
streamer = lambda do |c|
|
294
|
-
sin.write c
|
295
|
-
end
|
296
|
-
|
297
|
-
post_thread = Thread.new(Thread.current) do |parent|
|
298
|
-
bl = lambda do |rok|
|
299
|
-
if Net::HTTPOK === rok
|
300
|
-
_url = rok["RBBT-STREAMING-JOB-URL"]
|
301
|
-
@url = File.join(@url, File.basename(_url)) if _url
|
302
|
-
rok.read_body do |c,_a, _b|
|
303
|
-
sin.write c
|
304
|
-
end
|
305
|
-
sin.close
|
306
|
-
else
|
307
|
-
parent.raise "Error in RestClient: " << rok.message
|
308
|
-
end
|
309
|
-
end
|
310
|
-
|
311
|
-
Log.debug{ "RestClient execute: #{ url } - #{Misc.fingerprint task_params}" }
|
312
|
-
RestClient::Request.execute(:method => :post, :url => url, :payload => task_params, :block_response => bl)
|
313
|
-
end
|
314
|
-
|
315
|
-
reader = Zlib::GzipReader.new(sout)
|
316
|
-
res_io = Misc.open_pipe do |sin|
|
317
|
-
while c = reader.read(Misc::BLOCK_SIZE)
|
318
|
-
sin.write c
|
319
|
-
end
|
320
|
-
sin.close
|
321
|
-
@done = true
|
322
|
-
end
|
323
|
-
|
324
|
-
ConcurrentStream.setup(res_io, :threads => [post_thread])
|
325
|
-
end
|
326
|
-
end
|
327
|
-
|
328
279
|
def exec_job
|
329
280
|
res = _run_job(:exec)
|
330
281
|
load_res res, result_type == :array ? :json : result_type
|
@@ -372,3 +323,5 @@ class WorkflowRESTClient
|
|
372
323
|
end
|
373
324
|
end
|
374
325
|
end
|
326
|
+
|
327
|
+
require 'rbbt/rest/client/run'
|