scout-gear 7.3.0 → 8.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (134) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +44 -16
  3. data/Rakefile +6 -1
  4. data/VERSION +1 -1
  5. data/bin/scout +21 -7
  6. data/doc/lib/scout/path.md +35 -0
  7. data/doc/lib/scout/workflow/task.md +13 -0
  8. data/lib/rbbt-scout.rb +1 -0
  9. data/lib/scout/cmd.rb +24 -25
  10. data/lib/scout/concurrent_stream.rb +59 -39
  11. data/lib/scout/config.rb +1 -1
  12. data/lib/scout/exceptions.rb +10 -0
  13. data/lib/scout/log/color.rb +15 -12
  14. data/lib/scout/log/progress/report.rb +8 -6
  15. data/lib/scout/log/progress/util.rb +61 -54
  16. data/lib/scout/log/progress.rb +1 -1
  17. data/lib/scout/log/trap.rb +107 -0
  18. data/lib/scout/log.rb +115 -52
  19. data/lib/scout/meta_extension.rb +47 -6
  20. data/lib/scout/misc/digest.rb +12 -3
  21. data/lib/scout/misc/format.rb +24 -7
  22. data/lib/scout/misc/insist.rb +1 -1
  23. data/lib/scout/misc/monitor.rb +22 -0
  24. data/lib/scout/misc/system.rb +58 -0
  25. data/lib/scout/named_array.rb +73 -3
  26. data/lib/scout/offsite/ssh.rb +171 -0
  27. data/lib/scout/offsite/step.rb +83 -0
  28. data/lib/scout/offsite/sync.rb +55 -0
  29. data/lib/scout/offsite.rb +3 -0
  30. data/lib/scout/open/lock/lockfile.rb +587 -0
  31. data/lib/scout/open/lock.rb +9 -2
  32. data/lib/scout/open/remote.rb +16 -1
  33. data/lib/scout/open/stream.rb +146 -83
  34. data/lib/scout/open/util.rb +22 -3
  35. data/lib/scout/open.rb +5 -4
  36. data/lib/scout/path/find.rb +24 -11
  37. data/lib/scout/path/util.rb +40 -0
  38. data/lib/scout/persist/serialize.rb +19 -6
  39. data/lib/scout/persist.rb +29 -13
  40. data/lib/scout/resource/path.rb +57 -0
  41. data/lib/scout/resource/produce.rb +0 -8
  42. data/lib/scout/resource/util.rb +12 -5
  43. data/lib/scout/tmpfile.rb +7 -8
  44. data/lib/scout/tsv/attach.rb +177 -0
  45. data/lib/scout/tsv/change_id.rb +40 -0
  46. data/lib/scout/tsv/dumper.rb +74 -46
  47. data/lib/scout/tsv/index.rb +85 -87
  48. data/lib/scout/tsv/open.rb +160 -85
  49. data/lib/scout/tsv/parser.rb +142 -80
  50. data/lib/scout/tsv/path.rb +1 -2
  51. data/lib/scout/tsv/persist/adapter.rb +15 -45
  52. data/lib/scout/tsv/persist/fix_width_table.rb +3 -0
  53. data/lib/scout/tsv/persist/tokyocabinet.rb +6 -1
  54. data/lib/scout/tsv/persist.rb +4 -0
  55. data/lib/scout/tsv/stream.rb +204 -0
  56. data/lib/scout/tsv/transformer.rb +152 -0
  57. data/lib/scout/tsv/traverse.rb +96 -92
  58. data/lib/scout/tsv/util/filter.rb +9 -0
  59. data/lib/scout/tsv/util/reorder.rb +81 -0
  60. data/lib/scout/tsv/util/select.rb +78 -33
  61. data/lib/scout/tsv/util/unzip.rb +86 -0
  62. data/lib/scout/tsv/util.rb +60 -11
  63. data/lib/scout/tsv.rb +34 -4
  64. data/lib/scout/work_queue/socket.rb +6 -1
  65. data/lib/scout/work_queue/worker.rb +5 -2
  66. data/lib/scout/work_queue.rb +51 -20
  67. data/lib/scout/workflow/definition.rb +23 -3
  68. data/lib/scout/workflow/deployment/orchestrator.rb +245 -0
  69. data/lib/scout/workflow/deployment.rb +1 -0
  70. data/lib/scout/workflow/step/dependencies.rb +56 -10
  71. data/lib/scout/workflow/step/file.rb +5 -0
  72. data/lib/scout/workflow/step/info.rb +40 -7
  73. data/lib/scout/workflow/step/load.rb +1 -1
  74. data/lib/scout/workflow/step/provenance.rb +9 -7
  75. data/lib/scout/workflow/step/status.rb +43 -0
  76. data/lib/scout/workflow/step.rb +160 -49
  77. data/lib/scout/workflow/task/dependencies.rb +114 -0
  78. data/lib/scout/workflow/task/inputs.rb +40 -32
  79. data/lib/scout/workflow/task.rb +38 -102
  80. data/lib/scout/workflow/usage.rb +48 -18
  81. data/lib/scout/workflow.rb +4 -2
  82. data/lib/scout-gear.rb +2 -0
  83. data/lib/scout.rb +6 -0
  84. data/scout-gear.gemspec +52 -23
  85. data/scout_commands/doc +37 -0
  86. data/scout_commands/find +1 -0
  87. data/scout_commands/offsite +30 -0
  88. data/scout_commands/update +29 -0
  89. data/scout_commands/workflow/info +15 -3
  90. data/scout_commands/workflow/install +102 -0
  91. data/scout_commands/workflow/task +57 -9
  92. data/test/scout/offsite/test_ssh.rb +15 -0
  93. data/test/scout/offsite/test_step.rb +33 -0
  94. data/test/scout/offsite/test_sync.rb +36 -0
  95. data/test/scout/offsite/test_task.rb +0 -0
  96. data/test/scout/open/test_stream.rb +60 -58
  97. data/test/scout/path/test_find.rb +10 -1
  98. data/test/scout/resource/test_path.rb +6 -0
  99. data/test/scout/resource/test_produce.rb +15 -0
  100. data/test/scout/test_meta_extension.rb +25 -0
  101. data/test/scout/test_named_array.rb +24 -0
  102. data/test/scout/test_persist.rb +9 -2
  103. data/test/scout/test_tsv.rb +229 -2
  104. data/test/scout/test_work_queue.rb +65 -41
  105. data/test/scout/tsv/persist/test_tokyocabinet.rb +29 -1
  106. data/test/scout/tsv/test_attach.rb +227 -0
  107. data/test/scout/tsv/test_change_id.rb +98 -0
  108. data/test/scout/tsv/test_dumper.rb +1 -1
  109. data/test/scout/tsv/test_index.rb +49 -3
  110. data/test/scout/tsv/test_open.rb +160 -2
  111. data/test/scout/tsv/test_parser.rb +33 -2
  112. data/test/scout/tsv/test_persist.rb +2 -0
  113. data/test/scout/tsv/test_stream.rb +200 -0
  114. data/test/scout/tsv/test_transformer.rb +120 -0
  115. data/test/scout/tsv/test_traverse.rb +88 -3
  116. data/test/scout/tsv/test_util.rb +1 -0
  117. data/test/scout/tsv/util/test_reorder.rb +94 -0
  118. data/test/scout/tsv/util/test_select.rb +25 -11
  119. data/test/scout/tsv/util/test_unzip.rb +112 -0
  120. data/test/scout/work_queue/test_socket.rb +0 -1
  121. data/test/scout/workflow/deployment/test_orchestrator.rb +272 -0
  122. data/test/scout/workflow/step/test_dependencies.rb +68 -0
  123. data/test/scout/workflow/step/test_info.rb +18 -0
  124. data/test/scout/workflow/step/test_status.rb +30 -0
  125. data/test/scout/workflow/task/test_dependencies.rb +355 -0
  126. data/test/scout/workflow/task/test_inputs.rb +67 -14
  127. data/test/scout/workflow/test_definition.rb +18 -0
  128. data/test/scout/workflow/test_documentation.rb +24 -0
  129. data/test/scout/workflow/test_step.rb +112 -3
  130. data/test/scout/workflow/test_task.rb +0 -151
  131. data/test/scout/workflow/test_usage.rb +33 -6
  132. data/test/test_scout.rb +9 -0
  133. metadata +100 -8
  134. data/scout_commands/workflow/task_old +0 -706
@@ -1,5 +1,6 @@
1
1
  require_relative '../open'
2
2
  require_relative 'open'
3
+ require 'set'
3
4
 
4
5
  module Persist
5
6
  TRUE_STRINGS = Set.new ["true", "True", "TRUE", "t", "T", "1", "yes", "Yes", "YES", "y", "Y", "ON", "on"] unless defined? TRUE_STRINGS
@@ -19,7 +20,7 @@ module Persist
19
20
  type = type.to_sym if String === type
20
21
  type = SERIALIZER if type == :serializer
21
22
  case type
22
- when nil, :string, :integer, :float, :boolean, :file, :path
23
+ when nil, :string, :text, :integer, :float, :boolean, :file, :path, :select, :folder, :binary
23
24
  if IO === content || StringIO === content
24
25
  content.read
25
26
  else
@@ -46,8 +47,9 @@ module Persist
46
47
  def self.deserialize(serialized, type)
47
48
  type = type.to_sym if String === type
48
49
  type = SERIALIZER if type == :serializer
50
+
49
51
  case type
50
- when nil, :string, :file, :stream
52
+ when nil, :string, :text, :file, :stream, :select, :folder
51
53
  serialized
52
54
  when :path
53
55
  Path.setup(serialized)
@@ -56,7 +58,7 @@ module Persist
56
58
  when :float
57
59
  serialized.to_f
58
60
  when :boolean
59
- TRUE_STRINGS.include? serialized
61
+ TRUE_STRINGS.include? serialized.strip
60
62
  when :array
61
63
  serialized.split("\n")
62
64
  when :yaml
@@ -99,9 +101,18 @@ module Persist
99
101
  return save_drivers[type].call(file, content)
100
102
  end
101
103
  end
102
- serialized = serialize(content, type)
103
- Open.sensible_write(file, serialized, :force => true)
104
- return nil
104
+
105
+ if type == :binary
106
+ content.force_encoding("ASCII-8BIT") if content.respond_to? :force_encoding
107
+ Open.open(path, :mode => 'wb') do |f|
108
+ f.puts content
109
+ end
110
+ content
111
+ else
112
+ serialized = serialize(content, type)
113
+ Open.sensible_write(file, serialized, :force => true)
114
+ return nil
115
+ end
105
116
  end
106
117
 
107
118
  def self.load(file, type = :serializer)
@@ -118,6 +129,8 @@ module Persist
118
129
  end
119
130
 
120
131
  case type
132
+ when :binary
133
+ Open.read(file, :mode => 'rb')
121
134
  when :yaml
122
135
  Open.yaml(file)
123
136
  when :json
data/lib/scout/persist.rb CHANGED
@@ -21,7 +21,9 @@ module Persist
21
21
  def self.persistence_path(name, options = {})
22
22
  options = IndiferentHash.add_defaults options, :dir => Persist.cache_dir
23
23
  other_options = IndiferentHash.pull_keys options, :other
24
- TmpFile.tmp_for_file(name, options, other_options)
24
+ name = name.filename if name.respond_to?(:filename) && name.filename
25
+ persist_options = {}
26
+ TmpFile.tmp_for_file(name, options.merge(persist_options), other_options)
25
27
  end
26
28
 
27
29
  MEMORY_CACHE = {}
@@ -31,25 +33,31 @@ module Persist
31
33
  return yield if FalseClass === persist_options[:persist]
32
34
  file = persist_options[:path] || options[:path] || persistence_path(name, options)
33
35
 
34
- lockfile = persist_options[:lockfile] || options[:lockfile] || Persist.persistence_path(file + '.persist', {:dir => Persist.lock_dir})
35
-
36
- update = options[:update] || persist_options[:update]
37
- update = Open.mtime(update) if Path === update
38
- update = Open.mtime(file) >= update ? false : true if Time === update
39
-
40
36
  if type == :memory
41
37
  repo = options[:memory] || options[:repo] || MEMORY_CACHE
42
38
  repo[file] ||= yield
43
39
  return repo[file]
44
40
  end
45
41
 
42
+ update = options[:update] || persist_options[:update]
43
+ update = Open.mtime(update) if Path === update
44
+ update = Open.mtime(file) >= update ? false : true if Time === update
45
+
46
+ lockfile = persist_options[:lockfile] || options[:lockfile] || Persist.persistence_path(file + '.persist', {:dir => Persist.lock_dir})
47
+
46
48
  Open.lock lockfile do |lock|
47
49
  if Open.exist?(file) && ! update
48
50
  Persist.load(file, type)
49
51
  else
50
- return yield(file) if block.arity == 1
51
- res = yield
52
52
  begin
53
+ file = file.find if Path === file
54
+ return yield(file) if block.arity == 1
55
+ res = yield
56
+
57
+ if res.nil?
58
+ return Persist.load(file, type)
59
+ end
60
+
53
61
  Open.rm(file)
54
62
 
55
63
  if IO === res || StringIO === res
@@ -72,17 +80,25 @@ module Persist
72
80
  pres = Persist.save(res, file, type)
73
81
  res = pres unless pres.nil?
74
82
  end
75
- rescue
83
+ rescue Exception
84
+ Thread.handle_interrupt(Exception => :never) do
85
+ if Open.exist?(file)
86
+ Log.debug "Failed persistence #{file} - erasing"
87
+ Open.rm file
88
+ else
89
+ Log.debug "Failed persistence #{file}"
90
+ end
91
+ end
76
92
  raise $! unless options[:canfail]
77
- Log.debug "Could not persist #{type} on #{file}"
78
93
  end
79
94
  res
80
95
  end
81
96
  end
82
97
  end
83
98
 
84
- def self.memory(name, *args, &block)
85
- self.persist(name, :memory, *args, &block)
99
+ def self.memory(name, options = {}, &block)
100
+ options[:persist_path] ||= options[:path] ||= [name, options[:key]].compact * ":"
101
+ self.persist(name, :memory, options, &block)
86
102
  end
87
103
 
88
104
  end
@@ -1,9 +1,56 @@
1
1
  module Path
2
+ def produce(force = false)
3
+ return self if ! force && (Open.exist?(self) || @produced)
4
+ begin
5
+ if Resource === self.pkgdir
6
+ self.pkgdir.produce self
7
+ else
8
+ false
9
+ end
10
+ rescue ResourceNotFound
11
+ false
12
+ rescue
13
+ message = $!.message
14
+ message = "No exception message" if message.nil? || message.empty?
15
+ Log.warn "Error producing #{self}: #{message}"
16
+ false
17
+ ensure
18
+ @produced = true
19
+ end
20
+ end
21
+
22
+ def produce_with_extension(extension, *args)
23
+ begin
24
+ self.produce(*args)
25
+ rescue Exception
26
+ exception = $!
27
+ begin
28
+ self.set_extension(extension).produce(*args)
29
+ rescue Exception
30
+ raise exception
31
+ end
32
+ end
33
+ end
34
+
35
+ def produce_and_find(extension = nil, *args)
36
+ if extension
37
+ found = find_with_extension(extension, *args)
38
+ found.exists? ? found : produce_with_extension(extension, *args)
39
+ else
40
+ found = find
41
+ found.exists? ? found : produce(*args)
42
+ end
43
+ end
44
+
2
45
  def relocate
3
46
  return self if Open.exists?(self)
4
47
  Resource.relocate(self)
5
48
  end
6
49
 
50
+ def identify
51
+ Resource.identify(self)
52
+ end
53
+
7
54
  def open(*args, &block)
8
55
  produce
9
56
  Open.open(self, *args, &block)
@@ -17,4 +64,14 @@ module Path
17
64
  def write(*args, &block)
18
65
  Open.write(self.find, *args, &block)
19
66
  end
67
+
68
+ def list
69
+ found = produce_and_find('list')
70
+ Open.list(found)
71
+ end
72
+
73
+ def exists?
74
+ return true if Open.exists?(self.find)
75
+ self.produce
76
+ end
20
77
  end
@@ -149,11 +149,3 @@ module Resource
149
149
  end
150
150
 
151
151
  end
152
-
153
- module Path
154
- def produce(force = false)
155
- return self if ! force && Open.exist?(self)
156
- self.pkgdir.produce self if Resource === self.pkgdir
157
- return self
158
- end
159
- end
@@ -1,13 +1,13 @@
1
1
  module Resource
2
2
  def identify(path)
3
3
  return path unless path.start_with?("/")
4
- path_maps = path.path_maps || self.path_maps || Path.path_maps
4
+ path_maps = path.path_maps if Path === path
5
+ path_maps ||= self.path_maps || Path.path_maps
5
6
  path = File.expand_path(path)
6
7
  path += "/" if File.directory?(path)
7
8
 
8
9
  map_order ||= (path_maps.keys & Path.basic_map_order) + (path_maps.keys - Path.basic_map_order)
9
10
  map_order -= [:current, "current"]
10
- map_order << :current
11
11
 
12
12
  choices = []
13
13
  map_order.uniq.each do |name|
@@ -16,6 +16,7 @@ module Resource
16
16
  next if pattern.nil?
17
17
 
18
18
  pattern = pattern.sub('{PWD}', Dir.pwd)
19
+ pattern = pattern.sub('{HOME}', ENV["HOME"])
19
20
  if String === pattern and pattern.include?('{')
20
21
  regexp = "^" + pattern
21
22
  .gsub(/{(TOPLEVEL)}/,'(?<\1>[^/]+)')
@@ -34,14 +35,20 @@ module Resource
34
35
  end
35
36
  end
36
37
 
37
- Path.setup(choices.sort_by{|s| s.length }.first, self, nil, path_maps)
38
+ identified = choices.sort_by{|s| s.length }.first
39
+
40
+ Path.setup(identified || path, self, nil, path_maps)
38
41
  end
39
42
 
40
- def self.relocate(path)
41
- return path if Open.exists?(path)
43
+ def self.identify(path)
42
44
  resource = path.pkgdir if Path === path
43
45
  resource = Scout unless Resource === resource
44
46
  unlocated = resource.identify path
47
+ end
48
+
49
+ def self.relocate(path)
50
+ return path if Open.exists?(path)
51
+ unlocated = identify(path)
45
52
  unlocated.find
46
53
  end
47
54
  end
data/lib/scout/tmpfile.rb CHANGED
@@ -93,19 +93,18 @@ module TmpFile
93
93
  end
94
94
  end
95
95
 
96
+ SLASH_REPLACE = '·'
96
97
  def self.tmp_for_file(file, tmp_options = {}, other_options = {})
97
- tmp_for_file = IndiferentHash.process_options tmp_options, :file
98
+ tmp_for_file, prefix, key, persistence_dir = IndiferentHash.process_options tmp_options, :file, :prefix, :key, :dir
98
99
  return tmp_for_file unless tmp_for_file.nil?
99
100
 
100
- prefix = IndiferentHash.process_options tmp_options, :prefix
101
-
102
101
  if prefix.nil?
103
- perfile = file.to_s.gsub(/\//, '>')
102
+ perfile = file.to_s.sub(/\.b?gz$/,'')
104
103
  else
105
- perfile = prefix.to_s + ":" + file.to_s.gsub(/\//, '>')
104
+ perfile = prefix.to_s + ":" + file.to_s.sub(/\.b?gz$/,'')
106
105
  end
107
106
 
108
- perfile.sub!(/\.b?gz$/,'')
107
+ perfile += "[#{ key }]" if key
109
108
 
110
109
  if other_options.include? :filters
111
110
  other_options[:filters].each do |match,value|
@@ -113,10 +112,10 @@ module TmpFile
113
112
  end
114
113
  end
115
114
 
116
- persistence_dir = IndiferentHash.process_options(tmp_options, :dir) || TmpFile.tmpdir
115
+ persistence_dir = TmpFile.tmpdir if persistence_dir.nil?
117
116
  Path.setup(persistence_dir) unless Path === persistence_dir
118
117
 
119
- filename = perfile.gsub(/\s/,'_').gsub(/\//,'>')
118
+ filename = perfile.gsub(/\s/,'_').gsub('/', SLASH_REPLACE)
120
119
  clean_options = other_options.dup
121
120
  clean_options.delete :unnamed
122
121
  clean_options.delete "unnamed"
@@ -0,0 +1,177 @@
1
+ module TSV
2
+
3
+ def self.match_keys(source, other, match_key: nil, other_key: nil)
4
+ match_key = (source.all_fields & other.all_fields).first if match_key.nil?
5
+
6
+ if match_key.nil?
7
+ source.all_fields.collect do |f|
8
+ other_key = other.identify_field(f)
9
+ if other_key
10
+ other_key = other.key_field if other_key == :key
11
+ match_key = f
12
+ break
13
+ end
14
+ end
15
+ end
16
+
17
+ if match_key.nil?
18
+ other.all_fields.collect do |f|
19
+ match_key = source.identify_field(f)
20
+ if match_key
21
+ other_key = f
22
+ break
23
+ end
24
+ end
25
+ end
26
+
27
+ match_key = source.key_field if match_key.nil?
28
+
29
+ if other_key.nil?
30
+ other_key = other.identify_field(match_key)
31
+ end
32
+
33
+ other_key = other.key_field if other_key.nil?
34
+
35
+ match_key = :key if match_key == source.key_field
36
+ other_key = :key if other_key == other.key_field
37
+
38
+ [match_key, other_key]
39
+ end
40
+
41
+ def self.attach(source, other, target: nil, fields: nil, match_key: nil, other_key: nil, one2one: true, complete: false, insitu: nil, persist_input: false, bar: nil)
42
+ source = TSV::Transformer.new source unless TSV === source || TSV::Parser === source
43
+ other = TSV.open other, persist: persist_input unless TSV === other
44
+
45
+ fields = [fields] if String === fields
46
+
47
+ match_key, other_key = TSV.match_keys(source, other, match_key: match_key, other_key: other_key)
48
+
49
+ if TSV::Transformer === source
50
+ source.dumper = case target
51
+ when :stream
52
+ TSV::Dumper.new(source.options.merge(sep: "\t"))
53
+ when nil
54
+ TSV.setup({}, **source.options.dup)
55
+ else
56
+ target
57
+ end
58
+ end
59
+
60
+ other.with_unnamed do
61
+ source.with_unnamed do
62
+
63
+ other_key_name = other_key == :key ? other.key_field : other_key
64
+ other_key_name = other.fields[other_key_name] if Integer === other_key
65
+ fields = other.all_fields - [other_key_name, source.key_field] if fields.nil?
66
+
67
+ if other_key != :key
68
+ other = other.reorder other_key, fields, one2one: one2one
69
+ end
70
+
71
+ other_field_positions = other.identify_field(fields)
72
+
73
+ log_message = "Attach #{Log.fingerprint fields - source.fields} to #{Log.fingerprint source} (#{[match_key, other_key] * "=~"})"
74
+ Log.debug log_message
75
+ bar = log_message if TrueClass === bar
76
+
77
+ new = fields - source.fields
78
+
79
+ source.fields = (source.fields + fields).uniq
80
+
81
+ overlaps = source.identify_field(fields)
82
+
83
+ empty_other_values = case source.type
84
+ when :list
85
+ [nil] * other.fields.length
86
+ when :flat
87
+ []
88
+ when :double
89
+ [[]] * other.fields.length
90
+ end
91
+
92
+ insitu = TSV === source ? true : false if insitu.nil?
93
+
94
+ match_key_pos = source.identify_field(match_key)
95
+ source.traverse bar: bar, unnamed: true do |orig_key,current_values|
96
+ keys = (match_key == :key || match_key_pos == :key) ? [orig_key] : current_values[match_key_pos]
97
+ keys = [keys] unless Array === keys
98
+
99
+ current_values = current_values.dup unless insitu
100
+ keys.each do |current_key|
101
+ other_values = other[current_key]
102
+
103
+ if other_values.nil?
104
+ other_values = empty_other_values
105
+ elsif other.type == :flat
106
+ other_values = [other_values]
107
+ elsif other.type == :list && source.type == :double
108
+ other_values = other_values.collect{|v| [v] }
109
+ elsif other.type == :double && source.type == :list
110
+ other_values = other_values.collect{|v| v.first }
111
+ end
112
+
113
+ other_values = other_values.values_at *other_field_positions
114
+
115
+ other_values.zip(overlaps).each do |v,overlap|
116
+ if source.type == :list
117
+ current_values[overlap] = v if current_values[overlap].nil? || String === current_values[overlap] && current_values[overlap].empty?
118
+ else
119
+ current_values[overlap] ||= []
120
+ current_values[overlap].concat (v - current_values[overlap])
121
+ end
122
+ end
123
+ end
124
+ source[orig_key] = current_values unless insitu
125
+ nil
126
+ end
127
+
128
+ if complete && match_key == :key
129
+ empty_self_values = case source.type
130
+ when :list
131
+ [nil] * source.fields.length
132
+ when :flat
133
+ []
134
+ when :double
135
+ [[]] * source.fields.length
136
+ end
137
+ other.each do |other_key,other_values|
138
+ next if source.include?(other_key)
139
+ if other.type == :flat
140
+ other_values = [other_values]
141
+ elsif other.type == :list && source.type == :double
142
+ other_values = other_values.collect{|v| [v] }
143
+ elsif other.type == :double && source.type == :list
144
+ other_values = other_values.collect{|v| v.first }
145
+ end
146
+
147
+ new_values = case source.type
148
+ when :list
149
+ [nil] * source.fields.length
150
+ when :flat
151
+ []
152
+ when :double
153
+ source.fields.length.times.collect{ [] }
154
+ end
155
+
156
+ other_values.zip(overlaps).each do |v,overlap|
157
+ if false && overlap == :key
158
+ other_key = Array === v ? v : v.first
159
+ elsif source.type == :list
160
+ new_values[overlap] = v if v[overlap].nil? || String === v[overlap] && v[overlap].empty?
161
+ else
162
+ new_values[overlap].concat v
163
+ end
164
+ end
165
+ source[other_key] = new_values
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ source
172
+ end
173
+
174
+ def attach(*args, **kwargs)
175
+ TSV.attach(self, *args, **kwargs)
176
+ end
177
+ end
@@ -0,0 +1,40 @@
1
+ module TSV
2
+ def self.change_key(source, new_key_field, identifiers: nil, one2one: false, stream: false, keep: false, persist_identifiers: nil)
3
+ source = TSV::Parser.new source if String === source
4
+ if identifiers && source.identify_field(new_key_field, strict: true).nil?
5
+ identifiers = identifiers.nil? ? source.identifiers : identifiers
6
+ new = source.attach(identifiers, fields: [new_key_field], insitu: false, one2one: true, persist_input: persist_identifiers)
7
+ new = new.change_key(new_key_field, keep: keep, stream: stream, one2one: one2one)
8
+ return new
9
+ end
10
+
11
+ fields = source.fields.dup - [new_key_field]
12
+ fields.unshift source.key_field if keep
13
+ transformer = TSV::Transformer.new source
14
+ transformer.key_field = new_key_field
15
+ transformer.fields = fields
16
+ transformer.traverse key_field: new_key_field, fields: fields, one2one: one2one, unnamed: true do |k,v|
17
+ [k, v]
18
+ end
19
+
20
+ stream ? transformer : transformer.tsv
21
+ end
22
+
23
+ def change_key(*args, **kwargs)
24
+ TSV.change_key(self, *args, **kwargs)
25
+ end
26
+
27
+ def self.change_id(source, source_id, new_id, identifiers: nil, one2one: false, insitu: false)
28
+ source = TSV::Parser.new source if String === source
29
+
30
+ identifiers = identifiers.nil? ? source.identifiers : identifiers
31
+
32
+ new_fields = source.fields.dup
33
+ new_fields[new_fields.index(source_id)] = new_id
34
+ return source.attach(identifiers, fields: [new_id], insitu: insitu).slice(new_fields)
35
+ end
36
+
37
+ def change_id(*args, **kwargs)
38
+ TSV.change_id(self, *args, **kwargs)
39
+ end
40
+ end