rbbt-util 5.21.7 → 5.21.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/rbbt/resource/path.rb +1 -1
- data/lib/rbbt/rest/client/get.rb +15 -5
- data/lib/rbbt/rest/client/step.rb +8 -2
- data/lib/rbbt/tsv/util.rb +2 -0
- data/lib/rbbt/util/misc/concurrent_stream.rb +1 -1
- data/lib/rbbt/workflow.rb +5 -4
- data/lib/rbbt/workflow/accessor.rb +34 -35
- data/lib/rbbt/workflow/step/dependencies.rb +4 -3
- data/lib/rbbt/workflow/step/run.rb +4 -3
- data/lib/rbbt/workflow/task.rb +3 -1
- data/share/rbbt_commands/resource/produce +1 -1
- data/share/rbbt_commands/tsv/info +1 -1
- data/share/rbbt_commands/tsv/query +59 -0
- data/share/rbbt_commands/workflow/init +57 -0
- data/share/rbbt_commands/workflow/server +4 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 215819a1374483cea2057ae08343d6f03b71d41f
|
4
|
+
data.tar.gz: a22977fcc3e07a9eac6f544d77a61be31caddc9c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3f74b31e23ec63d0f47998a144c0149109804a3421b6f35562769d1488f5fe78b7f8469933a2a8f207c9106b1a8b8daff0ecdd8b81ff6d21eece27d734ab6734
|
7
|
+
data.tar.gz: 833a891002ef13c8bd6e8655475843af420130b621ba15113e5cddb466f9118eb93e7b0628c2a2a0c97750e24ee37ad25f2a4c851b8618958f691b312420aaa0
|
data/lib/rbbt/resource/path.rb
CHANGED
@@ -96,7 +96,7 @@ module Path
|
|
96
96
|
rsearch_paths = (resource and resource.respond_to?(:search_paths)) ? resource.search_paths : nil
|
97
97
|
key_elems = [where, caller_lib, rsearch_paths, paths]
|
98
98
|
key = Misc.digest(key_elems.inspect)
|
99
|
-
self.sub!('~/', Etc.getpwuid.dir + '/')
|
99
|
+
self.sub!('~/', Etc.getpwuid.dir + '/') if self.include? "~"
|
100
100
|
@path[key] ||= begin
|
101
101
|
paths = [paths, rsearch_paths, self.search_paths, SEARCH_PATHS].reverse.compact.inject({}){|acc,h| acc.merge! h; acc }
|
102
102
|
where = paths[:default] if where == :default
|
data/lib/rbbt/rest/client/get.rb
CHANGED
@@ -1,4 +1,13 @@
|
|
1
1
|
class WorkflowRESTClient
|
2
|
+
def self.encode(url)
|
3
|
+
begin
|
4
|
+
URI.encode(url)
|
5
|
+
rescue
|
6
|
+
Log.warn $!.message
|
7
|
+
url
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
2
11
|
def self.fix_hash(hash, fix_values = false)
|
3
12
|
fixed = {}
|
4
13
|
hash.each do |key, value|
|
@@ -59,7 +68,7 @@ class WorkflowRESTClient
|
|
59
68
|
res = capture_exception do
|
60
69
|
Misc.insist(2, 0.5) do
|
61
70
|
Log.debug{ "RestClient clean: #{ url } - #{Misc.fingerprint params}" }
|
62
|
-
res = RestClient.get(
|
71
|
+
res = RestClient.get(self.encode(url), :params => params)
|
63
72
|
raise TryAgain if res.code == 202
|
64
73
|
res
|
65
74
|
end
|
@@ -73,7 +82,8 @@ class WorkflowRESTClient
|
|
73
82
|
res = capture_exception do
|
74
83
|
Misc.insist(2, 0.5) do
|
75
84
|
Log.debug{ "RestClient get_raw: #{ url } - #{Misc.fingerprint params}" }
|
76
|
-
|
85
|
+
raise "No url" if url.nil?
|
86
|
+
res = RestClient.get(self.encode(url), :params => params)
|
77
87
|
raise TryAgain if res.code == 202
|
78
88
|
res
|
79
89
|
end
|
@@ -88,7 +98,7 @@ class WorkflowRESTClient
|
|
88
98
|
|
89
99
|
res = capture_exception do
|
90
100
|
Misc.insist(2, 0.5) do
|
91
|
-
RestClient.get(
|
101
|
+
RestClient.get(self.encode(url), :params => params)
|
92
102
|
end
|
93
103
|
end
|
94
104
|
|
@@ -106,7 +116,7 @@ class WorkflowRESTClient
|
|
106
116
|
|
107
117
|
WorkflowRESTClient.__prepare_inputs_for_restclient(params)
|
108
118
|
name = capture_exception do
|
109
|
-
RestClient.post(
|
119
|
+
RestClient.post(self.encode(url), params)
|
110
120
|
end
|
111
121
|
|
112
122
|
Log.debug{ "RestClient jobname returned for #{ url } - #{Misc.fingerprint params}: #{name}" }
|
@@ -122,7 +132,7 @@ class WorkflowRESTClient
|
|
122
132
|
params = fix_params params
|
123
133
|
|
124
134
|
res = capture_exception do
|
125
|
-
RestClient.post(
|
135
|
+
RestClient.post(self.encode(url), params)
|
126
136
|
end
|
127
137
|
|
128
138
|
begin
|
@@ -204,7 +204,7 @@ class WorkflowRESTClient
|
|
204
204
|
end
|
205
205
|
|
206
206
|
|
207
|
-
def fork
|
207
|
+
def fork(noload=false, semaphore=nil)
|
208
208
|
init_job(:asynchronous)
|
209
209
|
end
|
210
210
|
|
@@ -233,6 +233,7 @@ class WorkflowRESTClient
|
|
233
233
|
end
|
234
234
|
|
235
235
|
def join
|
236
|
+
init_job unless @url
|
236
237
|
Log.debug{ "Joining RestClient: #{path}" }
|
237
238
|
if IO === @result
|
238
239
|
res = @result
|
@@ -258,6 +259,7 @@ class WorkflowRESTClient
|
|
258
259
|
params = params.merge(:_format => [:string, :boolean, :tsv, :annotations,:array].include?(result_type.to_sym) ? :raw : :json )
|
259
260
|
Misc.insist 3, rand(2) + 1 do
|
260
261
|
begin
|
262
|
+
init_job if url.nil?
|
261
263
|
WorkflowRESTClient.get_raw(url, params)
|
262
264
|
rescue
|
263
265
|
Log.exception $!
|
@@ -291,7 +293,11 @@ class WorkflowRESTClient
|
|
291
293
|
(stream ? res.read : res).split("\n")
|
292
294
|
res.split("\n")
|
293
295
|
else
|
294
|
-
|
296
|
+
if IO === res
|
297
|
+
JSON.parse res.read
|
298
|
+
else
|
299
|
+
JSON.parse res
|
300
|
+
end
|
295
301
|
end
|
296
302
|
end
|
297
303
|
|
data/lib/rbbt/tsv/util.rb
CHANGED
@@ -94,7 +94,7 @@ module ConcurrentStream
|
|
94
94
|
@pids.each do |pid|
|
95
95
|
begin
|
96
96
|
Process.waitpid(pid, Process::WUNTRACED)
|
97
|
-
raise ProcessFailed.new "Error joining process #{pid} in #{self.inspect}" unless $?.success? or no_fail
|
97
|
+
raise ProcessFailed.new "Error joining process #{pid} in #{self.filename || self.inspect}" unless $?.success? or no_fail
|
98
98
|
rescue Errno::ECHILD
|
99
99
|
end
|
100
100
|
end
|
data/lib/rbbt/workflow.rb
CHANGED
@@ -326,10 +326,11 @@ module Workflow
|
|
326
326
|
|
327
327
|
inputs.each do |k,v|
|
328
328
|
default = defaults[k]
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
329
|
+
next unless (task_inputs.include?(k.to_sym) or task_inputs.include?(k.to_s))
|
330
|
+
next if default == v
|
331
|
+
next if (String === default and Symbol === v and v.to_s == default)
|
332
|
+
next if (Symbol === default and String === v and v == default.to_s)
|
333
|
+
real_inputs[k] = v
|
333
334
|
end
|
334
335
|
|
335
336
|
if real_inputs.empty?
|
@@ -373,13 +373,17 @@ class Step
|
|
373
373
|
end
|
374
374
|
end
|
375
375
|
|
376
|
+
def stalled?
|
377
|
+
! (done? || error? || aborted?) && ! running?
|
378
|
+
end
|
379
|
+
|
376
380
|
def error?
|
377
381
|
status == :error
|
378
382
|
end
|
379
383
|
|
380
384
|
def nopid?
|
381
385
|
pid = info[:pid]
|
382
|
-
pid.nil? && ! (status == :aborted || status == :done || status == :error)
|
386
|
+
pid.nil? && ! (status.nil? || status.nil? || status == :aborted || status == :done || status == :error)
|
383
387
|
end
|
384
388
|
|
385
389
|
def aborted?
|
@@ -542,46 +546,41 @@ module Workflow
|
|
542
546
|
else
|
543
547
|
all_deps << dep unless Proc === dep
|
544
548
|
end
|
545
|
-
case dep
|
546
|
-
when Array
|
547
|
-
wf, t, o = dep
|
548
|
-
|
549
|
-
wf.rec_dependencies(t).each do |d|
|
550
|
-
if Array === d
|
551
|
-
new = d.dup
|
552
|
-
else
|
553
|
-
new = [dep.first, d]
|
554
|
-
end
|
555
549
|
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
550
|
+
begin
|
551
|
+
case dep
|
552
|
+
when Array
|
553
|
+
wf, t, o = dep
|
554
|
+
|
555
|
+
wf.rec_dependencies(t).each do |d|
|
556
|
+
if Array === d
|
557
|
+
new = d.dup
|
561
558
|
else
|
562
|
-
new.
|
559
|
+
new = [dep.first, d]
|
563
560
|
end
|
564
|
-
end
|
565
561
|
|
566
|
-
|
567
|
-
|
562
|
+
if Hash === o and not o.empty?
|
563
|
+
if Hash === new.last
|
564
|
+
hash = new.last.dup
|
565
|
+
o.each{|k,v| hash[k] ||= v}
|
566
|
+
new[new.length-1] = hash
|
567
|
+
else
|
568
|
+
new.push o.dup
|
569
|
+
end
|
570
|
+
end
|
568
571
|
|
569
|
-
|
570
|
-
rec_deps = rec_dependencies(dep.to_sym)
|
571
|
-
all_deps.concat rec_deps
|
572
|
-
when DependencyBlock
|
573
|
-
all_deps << dep.dependency if dep.dependency
|
574
|
-
case dep.dependency
|
575
|
-
when Array
|
576
|
-
dep_wf, dep_task, dep_options = dep.dependency
|
577
|
-
if dep_task === Symbol
|
578
|
-
dep_rec_dependencies = dep_wf.rec_dependencies(dep_task.to_sym)
|
579
|
-
dep_rec_dependencies.collect!{|d| Array === d ? d : [dep_wf, d]}
|
580
|
-
all_deps.concat dep_rec_dependencies
|
572
|
+
all_deps << new
|
581
573
|
end
|
582
|
-
|
583
|
-
|
574
|
+
|
575
|
+
when String, Symbol
|
576
|
+
rec_deps = rec_dependencies(dep.to_sym)
|
577
|
+
all_deps.concat rec_deps
|
578
|
+
when DependencyBlock
|
579
|
+
dep = dep.dependency
|
580
|
+
raise TryAgain
|
584
581
|
end
|
582
|
+
rescue TryAgain
|
583
|
+
retry
|
585
584
|
end
|
586
585
|
end
|
587
586
|
all_deps.uniq
|
@@ -706,7 +705,7 @@ module Workflow
|
|
706
705
|
end
|
707
706
|
else
|
708
707
|
input_options = workflow.task_info(dep_task)[:input_options][i] || {}
|
709
|
-
if input_options[:stream]
|
708
|
+
if input_options[:stream] or true
|
710
709
|
#rec_dependency.run(true).grace unless rec_dependency.done? or rec_dependency.running?
|
711
710
|
_inputs[i] = rec_dependency
|
712
711
|
else
|
@@ -76,10 +76,11 @@ class Step
|
|
76
76
|
return if job.done? && ! job.dirty?
|
77
77
|
|
78
78
|
status = job.status.to_s
|
79
|
-
|
80
|
-
|
79
|
+
|
80
|
+
if defined?(WorkflowRESTClient) && WorkflowRESTClient::RemoteStep === job
|
81
|
+
return unless (status == 'done' or status == 'error' or status == 'aborted')
|
81
82
|
else
|
82
|
-
return if status == 'streaming'
|
83
|
+
return if status == 'streaming' and job.running?
|
83
84
|
end
|
84
85
|
|
85
86
|
if (status == 'error' || job.aborted?) && job.recoverable_error?
|
@@ -243,7 +243,8 @@ class Step
|
|
243
243
|
def produce(force=false, dofork=false)
|
244
244
|
return self if done? and not dirty?
|
245
245
|
|
246
|
-
if error? or aborted?
|
246
|
+
if error? or aborted? or stalled?
|
247
|
+
abort if stalled?
|
247
248
|
if force or aborted? or recoverable_error?
|
248
249
|
clean
|
249
250
|
else
|
@@ -253,13 +254,12 @@ class Step
|
|
253
254
|
|
254
255
|
clean if dirty? or (not running? and not done?)
|
255
256
|
|
256
|
-
no_load = :stream
|
257
257
|
if dofork
|
258
258
|
fork(true) unless started?
|
259
259
|
|
260
260
|
join unless done?
|
261
261
|
else
|
262
|
-
run(
|
262
|
+
run(true) unless started?
|
263
263
|
|
264
264
|
join unless done?
|
265
265
|
end
|
@@ -478,6 +478,7 @@ class Step
|
|
478
478
|
self
|
479
479
|
ensure
|
480
480
|
set_info :joined, true
|
481
|
+
@result = nil
|
481
482
|
end
|
482
483
|
end
|
483
484
|
end
|
data/lib/rbbt/workflow/task.rb
CHANGED
@@ -84,12 +84,14 @@ module Task
|
|
84
84
|
task_name, wf = wf, workflow if task_name.nil? and Symbol === wf or String === wf
|
85
85
|
next if task_name.nil?
|
86
86
|
task = wf.tasks[task_name.to_sym]
|
87
|
-
else
|
87
|
+
else
|
88
88
|
next
|
89
89
|
end
|
90
|
+
|
90
91
|
maps = (Array === dep and Hash === dep.last) ? dep.last.keys : []
|
91
92
|
raise "Dependency task not found: #{dep}" if task.nil?
|
92
93
|
next if seen.include? [wf, task.name]
|
94
|
+
|
93
95
|
seen << [wf, task.name]
|
94
96
|
new_inputs = task.inputs - maps
|
95
97
|
next unless new_inputs.any?
|
@@ -5,7 +5,7 @@ require 'rbbt/resource'
|
|
5
5
|
require 'rbbt/workflow'
|
6
6
|
|
7
7
|
options = SOPT.get <<EOF
|
8
|
-
-
|
8
|
+
-W--workflows* Workflows to use; 'all' for all in Rbbt.etc.workflows:
|
9
9
|
-r--requires* Files to require; 'all' for all in Rbbt.etc.requires:
|
10
10
|
-f--force Force the production if the file is already present
|
11
11
|
-h--help Help
|
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rbbt-util'
|
4
|
+
require 'rbbt/util/simpleopt'
|
5
|
+
|
6
|
+
$0 = "rbbt #{$previous_commands*""} #{ File.basename(__FILE__) }" if $previous_commands
|
7
|
+
|
8
|
+
options = SOPT.setup <<EOF
|
9
|
+
Query a TSV file
|
10
|
+
|
11
|
+
$ rbbt tsv query [options] <file.tsv> <key>
|
12
|
+
|
13
|
+
Display summary information for a TSV entry. Works with Tokyocabinet HDB and BDB.
|
14
|
+
|
15
|
+
-tch--tokyocabinet File is a TC HDB
|
16
|
+
-tcb--tokyocabinet_bd File is a TC BDB
|
17
|
+
-t--type* Type of tsv (single, list, double, flat)
|
18
|
+
-hh--header_hash* Change the character used to mark the header line (defaults to #)
|
19
|
+
-k--key_field* Change the key field
|
20
|
+
-f--field* Change the fields to display
|
21
|
+
-s--sep* Change the fields separator (default TAB)
|
22
|
+
-h--help Help
|
23
|
+
EOF
|
24
|
+
|
25
|
+
SOPT.usage if options[:help]
|
26
|
+
|
27
|
+
file, key = ARGV
|
28
|
+
|
29
|
+
file = STDIN if file == '-'
|
30
|
+
|
31
|
+
raise ParameterException, "Please specify the tsv file as argument" if file.nil?
|
32
|
+
|
33
|
+
options[:fields] = options[:fields].split(/[,\|]/) if options[:fields]
|
34
|
+
options[:header_hash] = options["header_hash"]
|
35
|
+
options[:sep] = options["sep"]
|
36
|
+
|
37
|
+
case
|
38
|
+
when options[:tokyocabinet]
|
39
|
+
tsv = Persist.open_tokyocabinet(file, false)
|
40
|
+
when options[:tokyocabinet_bd]
|
41
|
+
tsv = Persist.open_tokyocabinet(file, false, nil, TokyoCabinet::BDB)
|
42
|
+
else
|
43
|
+
tsv = TSV.open(file, options)
|
44
|
+
end
|
45
|
+
|
46
|
+
values = tsv[key]
|
47
|
+
|
48
|
+
head = "#{Log.color :magenta, tsv.key_field}: " << Log.color(:yellow, key)
|
49
|
+
puts head
|
50
|
+
puts (["-"] * Log.uncolor(head).length) * ""
|
51
|
+
values.zip(tsv.fields) do |value,field|
|
52
|
+
if Array === value
|
53
|
+
values = value.collect{|v| Log.color(:yellow, v)} * ", "
|
54
|
+
puts "#{Log.color :magenta, field}: " << values
|
55
|
+
else
|
56
|
+
puts "#{Log.color :magenta, field}: " << Log.color(:yellow, value)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rbbt-util'
|
4
|
+
require 'rbbt/workflow'
|
5
|
+
|
6
|
+
options = SOPT.setup <<EOF
|
7
|
+
|
8
|
+
Init a new workflow scaffold
|
9
|
+
|
10
|
+
$ rbbt workflow init <workflow>
|
11
|
+
EOF
|
12
|
+
|
13
|
+
workflow = ARGV.shift
|
14
|
+
if workflow.nil?
|
15
|
+
usage
|
16
|
+
puts
|
17
|
+
puts Log.color :magenta, "## Error"
|
18
|
+
puts
|
19
|
+
puts "No workflow name specified."
|
20
|
+
puts
|
21
|
+
exit -1
|
22
|
+
end
|
23
|
+
|
24
|
+
workflow_path = Path.setup(workflow) unless Path === workflow
|
25
|
+
lib_path = workflow_path + '/lib'
|
26
|
+
|
27
|
+
if Dir.exists?(workflow_path)
|
28
|
+
usage
|
29
|
+
puts
|
30
|
+
puts Log.color :magenta, "## Error"
|
31
|
+
puts
|
32
|
+
puts "The workflow '#{workflow}' already exists!"
|
33
|
+
puts
|
34
|
+
exit -1
|
35
|
+
end
|
36
|
+
|
37
|
+
template = <<-EOF
|
38
|
+
require 'rbbt/workflow'
|
39
|
+
|
40
|
+
module #{workflow}
|
41
|
+
extend Workflow
|
42
|
+
|
43
|
+
desc "Scaffold task"
|
44
|
+
task :scaffold_task => :string do
|
45
|
+
"Scaffold task"
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
EOF
|
50
|
+
|
51
|
+
Dir.mkdir(workflow_path)
|
52
|
+
workflow_file = workflow_path + '/workflow.rb'
|
53
|
+
File.write(workflow_file, template)
|
54
|
+
|
55
|
+
Dir.mkdir(lib_path)
|
56
|
+
lib_file = lib_path + '/.keep'
|
57
|
+
File.write(lib_file, '')
|
@@ -19,6 +19,7 @@ $ rbbt workflow server [options] <Workflow>
|
|
19
19
|
-RS--Rserve_session* Rserve session to use, otherwise start new one
|
20
20
|
-wd--workdir* Change the working directory of the workflow
|
21
21
|
-W--workflows* List of additional workflows to load
|
22
|
+
-R--requires* Require a list of files
|
22
23
|
--views* Directory with view templates
|
23
24
|
--stream Activate streaming of workflow tasks
|
24
25
|
--export* Export workflow tasks (asynchronous)
|
@@ -49,6 +50,7 @@ options[:Bind] ||= "0.0.0.0"
|
|
49
50
|
|
50
51
|
workflow = ARGV.shift
|
51
52
|
workflows = options[:workflows] || ""
|
53
|
+
requires = options[:requires] || ""
|
52
54
|
|
53
55
|
workflow = File.expand_path(workflow) if File.exist?(workflow)
|
54
56
|
|
@@ -73,7 +75,8 @@ TmpFile.with_file do |app_dir|
|
|
73
75
|
Open.write(app_dir.etc.target_workflow_sync_exports.find, sync_exports * "\n") if sync_exports
|
74
76
|
Open.write(app_dir.etc.target_workflow_exec_exports.find, exec_exports * "\n") if exec_exports
|
75
77
|
|
76
|
-
Open.write(app_dir.etc.workflows.find, workflows.split(/,\s*/)*"\n") if workflows
|
78
|
+
Open.write(app_dir.etc.workflows.find, workflows.split(/,\s*/)*"\n") if workflows and not workflows.empty?
|
79
|
+
Open.write(app_dir.etc.requires.find, requires.split(/,\s*/)*"\n") if requires and not requires.empty?
|
77
80
|
|
78
81
|
require 'rack'
|
79
82
|
ENV["RBBT_FINDER"] = "true" if options.include?(:finder)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rbbt-util
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.21.
|
4
|
+
version: 5.21.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Miguel Vazquez
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-08-
|
11
|
+
date: 2016-08-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -357,6 +357,7 @@ files:
|
|
357
357
|
- share/rbbt_commands/tsv/head
|
358
358
|
- share/rbbt_commands/tsv/info
|
359
359
|
- share/rbbt_commands/tsv/json
|
360
|
+
- share/rbbt_commands/tsv/query
|
360
361
|
- share/rbbt_commands/tsv/read
|
361
362
|
- share/rbbt_commands/tsv/slice
|
362
363
|
- share/rbbt_commands/tsv/sort
|
@@ -367,6 +368,7 @@ files:
|
|
367
368
|
- share/rbbt_commands/workflow/cmd
|
368
369
|
- share/rbbt_commands/workflow/example
|
369
370
|
- share/rbbt_commands/workflow/info
|
371
|
+
- share/rbbt_commands/workflow/init
|
370
372
|
- share/rbbt_commands/workflow/install
|
371
373
|
- share/rbbt_commands/workflow/jobs
|
372
374
|
- share/rbbt_commands/workflow/knowledge_base
|