scout-gear 10.7.2 → 10.7.4

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +47 -32
  3. data/VERSION +1 -1
  4. data/bin/scout +15 -2
  5. data/lib/scout/association/index.rb +4 -0
  6. data/lib/scout/association/item.rb +1 -1
  7. data/lib/scout/association.rb +32 -10
  8. data/lib/scout/entity/identifiers.rb +2 -2
  9. data/lib/scout/entity/property.rb +2 -2
  10. data/lib/scout/knowledge_base/enrichment.rb +9 -0
  11. data/lib/scout/knowledge_base/entity.rb +152 -0
  12. data/lib/scout/knowledge_base/list.rb +95 -0
  13. data/lib/scout/knowledge_base/query.rb +96 -0
  14. data/lib/scout/knowledge_base/registry.rb +175 -0
  15. data/lib/scout/knowledge_base/traverse.rb +329 -0
  16. data/lib/scout/knowledge_base.rb +91 -0
  17. data/lib/scout/persist/engine/tokyocabinet.rb +85 -77
  18. data/lib/scout/persist/tsv/adapter/base.rb +8 -22
  19. data/lib/scout/tsv/annotation.rb +4 -4
  20. data/lib/scout/tsv/index.rb +0 -2
  21. data/lib/scout/tsv/parser.rb +11 -1
  22. data/lib/scout/tsv/stream.rb +3 -3
  23. data/lib/scout/tsv/transformer.rb +12 -0
  24. data/lib/scout/tsv/util/process.rb +2 -2
  25. data/lib/scout/tsv.rb +2 -0
  26. data/lib/scout/workflow/definition.rb +6 -2
  27. data/lib/scout/workflow/deployment/trace.rb +1 -1
  28. data/lib/scout/workflow/step/dependencies.rb +3 -6
  29. data/lib/scout/workflow/step/info.rb +17 -3
  30. data/lib/scout/workflow/task/info.rb +99 -0
  31. data/lib/scout/workflow/task.rb +1 -0
  32. data/scout-gear.gemspec +27 -7
  33. data/scout_commands/doc +3 -3
  34. data/scout_commands/kb/config +33 -0
  35. data/scout_commands/kb/entities +35 -0
  36. data/scout_commands/kb/list +39 -0
  37. data/scout_commands/{db → kb}/query +6 -11
  38. data/scout_commands/{db → kb}/register +9 -8
  39. data/scout_commands/{db → kb}/show +6 -16
  40. data/scout_commands/kb/traverse +66 -0
  41. data/scout_commands/workflow/task +7 -2
  42. data/test/data/person/brothers +1 -1
  43. data/test/scout/entity/test_identifiers.rb +3 -3
  44. data/test/scout/knowledge_base/test_enrichment.rb +0 -0
  45. data/test/scout/knowledge_base/test_entity.rb +38 -0
  46. data/test/scout/knowledge_base/test_list.rb +40 -0
  47. data/test/scout/knowledge_base/test_query.rb +39 -0
  48. data/test/scout/knowledge_base/test_registry.rb +35 -0
  49. data/test/scout/knowledge_base/test_traverse.rb +245 -0
  50. data/test/scout/persist/test_tsv.rb +1 -0
  51. data/test/scout/test_association.rb +32 -3
  52. data/test/scout/test_entity.rb +0 -15
  53. data/test/scout/test_knowledge_base.rb +27 -0
  54. data/test/scout/test_tsv.rb +15 -0
  55. data/test/scout/tsv/test_parser.rb +4 -0
  56. data/test/scout/tsv/test_transformer.rb +13 -0
  57. data/test/scout/workflow/step/test_info.rb +11 -0
  58. data/test/scout/workflow/task/test_info.rb +22 -0
  59. data/test/test_helper.rb +17 -0
  60. metadata +26 -9
@@ -0,0 +1,175 @@
1
+ require 'scout/association'
2
+ require 'scout/association/item'
3
+
4
+ class KnowledgeBase
5
+ def register(name, file = nil, options = {}, &block)
6
+ file = file.find if Path === file
7
+ @registry ||= IndiferentHash.setup({})
8
+ if block_given?
9
+ block.define_singleton_method(:filename) do name.to_s end
10
+ Log.debug("Registering #{ name } from code block")
11
+ @registry[name] = [block, options]
12
+ else
13
+ Log.debug("Registering #{ name }: #{ Log.fingerprint file } #{Log.fingerprint options}")
14
+ @registry[name] = [file, options]
15
+ end
16
+ end
17
+
18
+ def all_databases
19
+ return [] unless @registry
20
+ @registry.keys
21
+ end
22
+
23
+ def include?(name)
24
+ all_databases.include? name
25
+ end
26
+
27
+ def fields(name)
28
+ @fields ||= {}
29
+ @fields[name] ||= get_index(name).fields
30
+ end
31
+
32
+ def description(name)
33
+ @descriptions ||= {}
34
+ @descriptions[name] ||= get_index(name).key_field.split("~")
35
+ end
36
+
37
+ def source(name)
38
+ description(name)[0]
39
+ end
40
+
41
+ def target(name)
42
+ description(name)[1]
43
+ end
44
+
45
+ def undirected(name)
46
+ description(name).length == 3
47
+ end
48
+
49
+ alias undirected? undirected
50
+
51
+ def get_index(name, options = {})
52
+ name = name.to_s
53
+ options[:namespace] ||= self.namespace unless self.namespace.nil?
54
+ @indices ||= IndiferentHash.setup({})
55
+ @indices[[name, options]] ||=
56
+ begin
57
+ if options.empty?
58
+ key = name.to_s
59
+ elsif options[:key]
60
+ key = options[:key]
61
+ key = name if key == :name
62
+ else
63
+ fp = Misc.digest(options)
64
+ key = name.to_s + "_" + fp
65
+ end
66
+
67
+ Persist.memory("Index:" << [key, dir] * "@") do
68
+ options = options.dup
69
+
70
+ persist_dir = dir
71
+ persist_path = persist_dir[key].find
72
+ file, registered_options = registry[name]
73
+
74
+ options = IndiferentHash.add_defaults options, registered_options if registered_options and registered_options.any?
75
+ options = IndiferentHash.add_defaults options, :persist_path => persist_path, :persist_dir => persist_dir, :persist => true
76
+
77
+ if entity_options
78
+ options[:entity_options] ||= {}
79
+ entity_options.each do |type, info|
80
+ options[:entity_options][type] ||= {}
81
+ options[:entity_options][type] = IndiferentHash.add_defaults options[:entity_options][type], info
82
+ end
83
+ end
84
+
85
+ persist_options = IndiferentHash.pull_keys options, :persist
86
+ persist_options = IndiferentHash.add_defaults persist_options
87
+
88
+ index = if persist_path.exists? and persist_options[:persist] and not persist_options[:update]
89
+ Log.low "Re-opening index #{ name } from #{ Log.fingerprint persist_path }. #{options}"
90
+ Association.index(file, **options, persist_options: persist_options.dup)
91
+ else
92
+ options = IndiferentHash.add_defaults options, registered_options if registered_options
93
+ raise "Repo #{ name } not found and not registered" if file.nil?
94
+ Log.medium "Opening index #{ name } from #{ Log.fingerprint file }. #{options}"
95
+ file = file.call if Proc === file
96
+ Association.index(file, **options, persist_options: persist_options.dup)
97
+ end
98
+
99
+ index.namespace = self.namespace unless self.namespace
100
+
101
+ index
102
+ end
103
+ end
104
+ end
105
+
106
+ def get_database(name, options = {})
107
+ options = options.dup
108
+ if self.namespace == options[:namespace]
109
+ options.delete(:namespace)
110
+ end
111
+ @databases ||= IndiferentHash.setup({})
112
+ @databases[[name, options]] ||=
113
+ begin
114
+ fp = Log.fingerprint([name,options])
115
+
116
+ if options.empty?
117
+ key = name.to_s
118
+ else
119
+ fp = Misc.digest(options)
120
+ key = name.to_s + "_" + fp
121
+ end
122
+
123
+ options[:namespace] ||= self.namespace unless self.namespace.nil?
124
+
125
+ key += '.database'
126
+ Persist.memory("Database:" << [key, dir] * "@") do
127
+ options = options.dup
128
+
129
+ persist_dir = dir
130
+ persist_path = persist_dir[key].find
131
+ file, registered_options = registry[name]
132
+
133
+ options = IndiferentHash.add_defaults options, registered_options if registered_options and registered_options.any?
134
+ options = IndiferentHash.add_defaults options, :persist_path => persist_path, :persist => true
135
+
136
+ if entity_options
137
+ options[:entity_options] ||= {}
138
+ entity_options.each do |type, info|
139
+ options[:entity_options][type] ||= {}
140
+ options[:entity_options][type] = IndiferentHash.add_defaults options[:entity_options][type], info
141
+ end
142
+ end
143
+
144
+ persist_options = IndiferentHash.pull_keys options, :persist
145
+
146
+ database = if persist_path.exists? and persist_options[:persist] and not persist_options[:update]
147
+ Log.low "Re-opening database #{ name } from #{ Log.fingerprint persist_path }. #{options}"
148
+ #Association.database(file, **options, persist_options: persist_options)
149
+ Association.database(file, **options.merge(persist_options: persist_options))
150
+ else
151
+ options = IndiferentHash.add_defaults options, registered_options if registered_options
152
+ undirected = IndiferentHash.process_options options, :undirected
153
+ raise "Repo #{ name } not found and not registered" if file.nil?
154
+ Log.medium "Opening database #{ name } from #{ Log.fingerprint file }. #{options}"
155
+ file = file.call if Proc === file
156
+ #Association.database(file, **options, persist_options: persist_options)
157
+ Association.database(file, **options.merge(persist_options: persist_options))
158
+ end
159
+
160
+ database.namespace = self.namespace if self.namespace
161
+
162
+ database
163
+ end
164
+ end
165
+ end
166
+
167
+ def index_fields(name)
168
+ get_index(name).fields
169
+ end
170
+
171
+ def produce(name, *rest,&block)
172
+ register(name, *rest, &block)
173
+ get_index(name)
174
+ end
175
+ end
@@ -0,0 +1,329 @@
1
+ class KnowledgeBase
2
+
3
+ class Traverser
4
+ attr_accessor :rules, :assignments, :matches, :kb
5
+
6
+ def initialize(kb, rules = [])
7
+ @kb = kb
8
+ @rules = rules
9
+ @assignments = {}
10
+ @matches = {}
11
+ end
12
+
13
+ def wildcard(name)
14
+ return name unless is_wildcard?(name)
15
+ assignments[name] || name
16
+ end
17
+
18
+ def is_wildcard?(name)
19
+ name[0] == '?'
20
+ end
21
+
22
+ def is_list?(name)
23
+ name[0] == ':'
24
+ end
25
+
26
+ def identify(db, source, target)
27
+ source_entities = if is_wildcard? source
28
+ assignments[source] || :all
29
+ elsif is_list? source
30
+ kb.load_list(source[1..-1])
31
+ else
32
+ kb.identify_source db, source
33
+ end
34
+
35
+ target_entities = if is_wildcard? target
36
+ assignments[target] || :all
37
+ elsif is_list? target
38
+ kb.load_list(target[1..-1])
39
+ else
40
+ kb.identify_target db, target
41
+ end
42
+
43
+ source_entities = [source_entities] unless Array === source_entities or source_entities == :all
44
+ target_entities = [target_entities] unless Array === target_entities or target_entities == :all
45
+
46
+ [source_entities, target_entities]
47
+ end
48
+
49
+ def reassign(matches, source, target)
50
+ #assignments[source] = (matches.any? ? matches.collect{|m| m.source_entity }.uniq : nil) if is_wildcard? source
51
+ #assignments[target] = (matches.any? ? matches.collect{|m| m.target_entity }.uniq : nil) if is_wildcard? target
52
+ assignments[source] = (matches.any? ? matches.source_entity.uniq : nil) if is_wildcard? source
53
+ assignments[target] = (matches.any? ? matches.target_entity.uniq : nil) if is_wildcard? target
54
+ end
55
+
56
+ def clean_matches(rules, all_matches, assignments)
57
+ paths = {}
58
+
59
+ rules.zip(all_matches).each do |rule, matches|
60
+ source, db, target = rule.split /\s+/
61
+ next if matches.nil?
62
+
63
+ if is_wildcard? source
64
+ assigned = assignments[source] || []
65
+ matches = matches.select{|m| assigned.include? m.partition("~").first }
66
+ end
67
+
68
+ if is_wildcard? target
69
+ assigned = assignments[target] || []
70
+ matches = matches.select{|m| assigned.include? m.partition("~").last }
71
+ end
72
+
73
+ paths[rule] = matches
74
+ end
75
+
76
+ paths
77
+ end
78
+
79
+ def _fp(rules, clean_matches, assignments)
80
+ return true if rules.empty?
81
+
82
+ rule, *rest = rules
83
+ source, db, target = rule.split /\s+/
84
+
85
+ wildcard_source = is_wildcard? source
86
+ wildcard_target = is_wildcard? target
87
+
88
+ paths = {}
89
+ matches = clean_matches[rule]
90
+ matches.each do |match|
91
+ new_assignments = nil
92
+ match_source, _sep, match_target = match.partition "~"
93
+
94
+ if wildcard_source
95
+ next if assignments[source] and assignments[source] != match_source
96
+ new_assignments ||= assignments.dup
97
+ new_assignments[source] = match_source
98
+ end
99
+
100
+ if wildcard_target
101
+ next if assignments[target] and assignments[target] != match_target
102
+ new_assignments ||= assignments.dup
103
+ new_assignments[target] = match_target
104
+ end
105
+
106
+ new_paths = _fp(rest, clean_matches, new_assignments)
107
+ next unless new_paths
108
+ paths[match] = new_paths
109
+ end
110
+
111
+ return false if paths.empty?
112
+
113
+ paths
114
+ end
115
+
116
+ def _ep(paths)
117
+ found = []
118
+ paths.each do |match,_next|
119
+ case _next
120
+ when TrueClass
121
+ found << [match]
122
+ when FalseClass
123
+ next
124
+ else
125
+ _ep(_next).each do |_n|
126
+ found << [match] + _n
127
+ end
128
+ end
129
+ end
130
+ found
131
+ end
132
+
133
+ def find_paths(rules, all_matches, assignments)
134
+ clean_matches = clean_matches(rules, all_matches, assignments)
135
+
136
+ path_hash = _fp(rules, clean_matches, {})
137
+
138
+ return [] unless path_hash
139
+ _ep(path_hash).collect do |path|
140
+ path.zip(clean_matches.values_at(*rules)).collect do |item, matches|
141
+ matches.select{|m| m == item }.first
142
+ end
143
+ end
144
+ end
145
+
146
+ def traverse_db(db, source, target, conditions)
147
+ source_entities, target_entities = identify db, source, target
148
+
149
+ options = {:source => source_entities, :target => target_entities}
150
+ Log.debug "Traversing #{ db }: #{Log.fingerprint options}"
151
+ matches = kb.subset(db, options)
152
+
153
+ if conditions
154
+ Misc.tokenize(conditions).each do |condition|
155
+ if condition.index "="
156
+ key, value = condition.split("=")
157
+ matches = matches.select{|m| Misc.match_value(m.info[key.strip], value)}
158
+ else
159
+ matches = matches.select{|m| m.info[condition.strip].to_s =~ /\btrue\b/}
160
+ end
161
+ end
162
+ end
163
+
164
+ matches
165
+ end
166
+
167
+ def id_dbs(db)
168
+ # ToDo: Revise this, I'm not sure what id does anymore
169
+ # I think it deals with syndication
170
+ if db.include? '?'
171
+ all_dbs = kb.registry.keys.collect{|k| k.to_s }
172
+ _name, _sep, _kb = db.partition("@")
173
+ case
174
+ when _name[0] == '?'
175
+ dbs = all_dbs.select{|_db|
176
+ n,_s,d=_db.partition("@");
177
+ d.nil? or d.empty? or (d == _kb and assignments[_name].include?(n))
178
+ }
179
+ when _kb[0] == '?'
180
+ dbs = all_dbs.select{|_db| n,_s,d=_db.partition("@"); n == _name and assignments[_kb].include?(d) }
181
+ end
182
+ else
183
+ dbs = [db]
184
+ end
185
+
186
+ dbs
187
+ end
188
+
189
+ def traverse(nopaths = false)
190
+ all_matches = []
191
+ path_rules = []
192
+ acc_var = nil
193
+ pre_acc_var_assignments = nil
194
+ rules.each do |rule|
195
+ rule = rule.strip
196
+ next if rule.empty?
197
+
198
+ if m = rule.match(/([^\s]+)\s+([^\s=]+)\s+([^\s]+)(?:\s+-\s+(.*))?/)
199
+ Log.debug "Traverse rule: #{rule}"
200
+ path_rules << rule
201
+
202
+ source, db, target, conditions = m.captures
203
+
204
+ dbs = id_dbs(db)
205
+
206
+ rule_matches = nil
207
+ dbs.each do |_db|
208
+ matches = traverse_db(_db, source, target, conditions)
209
+
210
+ next if matches.nil? or matches.empty?
211
+
212
+ # ToDo: Revise this, I'm not sure what id does anymore
213
+ #
214
+ #if db.include? '?'
215
+ # _name, _sep, _kb = db.partition("@")
216
+ # case
217
+ # when _kb[0] == '?'
218
+ # assignments[_kb] ||= []
219
+ # assignments[_kb] << _db.partition("@").reject{|p| p.empty?}.last
220
+ # when _name[0] == '?'
221
+ # assignments[_name] ||= []
222
+ # assignments[_name] << _db.partition("@").first
223
+ # end
224
+ #end
225
+
226
+ if rule_matches.nil?
227
+ rule_matches = matches
228
+ else
229
+ matches.each do |m|
230
+ rule_matches << m
231
+ end
232
+ end
233
+
234
+ assignments.each{|k,v| v.uniq! if v}
235
+ end
236
+
237
+ reassign rule_matches, source, target if rule_matches
238
+
239
+ all_matches << rule_matches
240
+
241
+ elsif m = rule.match(/([^\s=]+)\s*=([^\s]*)\s*(.*)/)
242
+ Log.debug "Assign rule: #{rule}"
243
+ var, db, value_str = m.captures
244
+ names = value_str.split(",").collect{|v| v.strip}
245
+ if db.empty?
246
+ ids = names
247
+ else
248
+ dbs = id_dbs(db)
249
+ names = names.collect{|name| assignments.include?(name) ? assignments[name] : name}.flatten
250
+ ids = names.collect{|name|
251
+ id = nil
252
+ dbs.each do |db|
253
+ sid, tid = identify db, name, name
254
+ id = (sid + tid).compact.first
255
+ break if id
256
+ end
257
+ id
258
+ }
259
+ end
260
+ assignments[var] = ids
261
+
262
+ elsif m = rule.match(/(\?[^\s{]+)\s*{/)
263
+ acc_var = m.captures.first
264
+ pre_acc_var_assignments = assignments.dup
265
+ Log.debug "Start assign block: #{acc_var}"
266
+ elsif m = rule.match(/^\s*}\s*$/)
267
+ Log.debug "Close assign block: #{acc_var}"
268
+ saved_assign = assignments[acc_var]
269
+ assignments.clear
270
+ assignments.merge!(pre_acc_var_assignments)
271
+ pre_acc_var_assignments = nil
272
+ assignments[acc_var] = saved_assign
273
+ all_matches = []
274
+ path_rules = []
275
+ else
276
+ raise "Rule not understood: #{rule}"
277
+ end
278
+ end
279
+
280
+ return [assignments, nil] if nopaths
281
+
282
+ Log.debug "Finding paths: #{all_matches.length}"
283
+ paths = find_paths path_rules, all_matches, assignments
284
+ Log.debug "Found paths: #{paths.length}"
285
+
286
+ [assignments, paths]
287
+ end
288
+
289
+ #def traverse
290
+ # all_matches = []
291
+
292
+ # rules.each do |rule|
293
+ # rule = rule.strip
294
+ # next if rule.empty?
295
+ # source, db, target, conditions = rule.match(/([^\s]+)\s+([^\s]+)\s+([^\s]+)(?:\s+-\s+([^\s]+))?/).captures
296
+
297
+ # source_entities, target_entities = identify db, source, target
298
+
299
+ # matches = kb.subset(db, :source => source_entities, :target => target_entities)
300
+
301
+ # if conditions
302
+ # conditions.split(/\s+/).each do |condition|
303
+ # if condition.index "="
304
+ # key, value = conditions.split("=")
305
+ # matches = matches.select{|m| m.info[key.strip].to_s =~ /\b#{value.strip}\b/}
306
+ # else
307
+ # matches = matches.select{|m| m.info[condition.strip].to_s =~ /\btrue\b/}
308
+ # end
309
+ # end
310
+ # end
311
+
312
+ # reassign matches, source, target
313
+
314
+ # all_matches << matches
315
+ # end
316
+
317
+ # paths = find_paths rules, all_matches, assignments
318
+
319
+ # [assignments, paths]
320
+ #end
321
+
322
+ end
323
+
324
+ def traverse(rules, nopaths=false)
325
+ traverser = KnowledgeBase::Traverser.new self, rules
326
+ traverser.traverse nopaths
327
+ end
328
+
329
+ end
@@ -0,0 +1,91 @@
1
+ require_relative 'association'
2
+ require_relative 'association/item'
3
+ require_relative 'knowledge_base/registry'
4
+ require_relative 'knowledge_base/entity'
5
+ require_relative 'knowledge_base/query'
6
+ require_relative 'knowledge_base/traverse'
7
+ require_relative 'knowledge_base/list'
8
+ #require 'scout/knowledge_base/query'
9
+ #require 'scout/knowledge_base/syndicate'
10
+
11
+ class KnowledgeBase
12
+
13
+ attr_accessor :dir, :namespace, :registry, :entity_options, :format, :identifier_files
14
+
15
+ def initialize(dir, namespace = nil)
16
+ @dir = Path.setup(dir.dup)
17
+
18
+ @namespace = namespace
19
+
20
+ @identifier_files = []
21
+
22
+ @registry ||= IndiferentHash.setup({})
23
+ @entity_options ||= IndiferentHash.setup({})
24
+
25
+ @format ||= IndiferentHash.setup({})
26
+ @descriptions ||= IndiferentHash.setup({})
27
+ @indices ||= IndiferentHash.setup({})
28
+ end
29
+
30
+ def config_file(name)
31
+ @dir.config[name.to_s]
32
+ end
33
+
34
+ def save_variable(name)
35
+ file = config_file(name)
36
+ variable = "@#{name}".to_sym
37
+ Open.write(file, self.instance_variable_get(variable).to_yaml)
38
+ end
39
+
40
+ def load_variable(name)
41
+ file = config_file(name)
42
+ variable = "@#{name}".to_sym
43
+ self.instance_variable_set(variable, YAML.load(Open.read(file))) if file.exists?
44
+ end
45
+
46
+ def save
47
+ save_variable(:namespace)
48
+ save_variable(:registry)
49
+ save_variable(:entity_options)
50
+ save_variable(:identifier_files)
51
+ end
52
+
53
+ def load
54
+ load_variable(:namespace)
55
+ load_variable(:registry)
56
+ load_variable(:entity_options)
57
+ load_variable(:identifier_files)
58
+ end
59
+
60
+ def self.load(dir)
61
+ dir = Path.setup("var").knowledge_base[dir.to_s] if Symbol === dir
62
+ kb = KnowledgeBase.new dir
63
+ kb.load
64
+ kb
65
+ end
66
+
67
+ def info(name)
68
+
69
+ source = self.source(name)
70
+ target = self.target(name)
71
+ source_type = self.source_type(name)
72
+ target_type = self.target_type(name)
73
+ fields = self.fields(name)
74
+ source_entity_options = self.entity_options_for source_type, name
75
+ target_entity_options = self.entity_options_for target_type, name
76
+ undirected = self.undirected(name) == 'undirected'
77
+
78
+ info = {
79
+ :source => source,
80
+ :target => target,
81
+ :source_type => source_type,
82
+ :target_type => target_type,
83
+ :source_entity_options => source_entity_options,
84
+ :target_entity_options => target_entity_options,
85
+ :fields => fields,
86
+ :undirected => undirected,
87
+ }
88
+
89
+ info
90
+ end
91
+ end