etna 0.1.28 → 0.1.33

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/etna.completion +115 -1
  3. data/lib/commands.rb +30 -0
  4. data/lib/etna/application.rb +4 -0
  5. data/lib/etna/auth.rb +25 -0
  6. data/lib/etna/client.rb +43 -6
  7. data/lib/etna/clients/base_client.rb +2 -3
  8. data/lib/etna/clients/janus.rb +1 -0
  9. data/lib/etna/clients/janus/client.rb +19 -0
  10. data/lib/etna/clients/janus/models.rb +7 -1
  11. data/lib/etna/clients/janus/workflows.rb +1 -0
  12. data/lib/etna/clients/janus/workflows/generate_token_workflow.rb +77 -0
  13. data/lib/etna/clients/magma/models.rb +13 -1
  14. data/lib/etna/clients/magma/workflows/create_project_workflow.rb +1 -1
  15. data/lib/etna/clients/magma/workflows/crud_workflow.rb +19 -2
  16. data/lib/etna/clients/magma/workflows/file_linking_workflow.rb +3 -1
  17. data/lib/etna/clients/magma/workflows/materialize_magma_record_files_workflow.rb +43 -28
  18. data/lib/etna/clients/magma/workflows/model_synchronization_workflow.rb +1 -1
  19. data/lib/etna/clients/magma/workflows/update_attributes_from_csv_workflow.rb +19 -6
  20. data/lib/etna/clients/magma/workflows/walk_model_tree_workflow.rb +33 -6
  21. data/lib/etna/clients/metis/client.rb +6 -1
  22. data/lib/etna/clients/metis/models.rb +15 -0
  23. data/lib/etna/clients/metis/workflows/metis_download_workflow.rb +15 -11
  24. data/lib/etna/clients/metis/workflows/metis_upload_workflow.rb +83 -13
  25. data/lib/etna/clients/metis/workflows/sync_metis_data_workflow.rb +43 -79
  26. data/lib/etna/command.rb +1 -0
  27. data/lib/etna/cwl.rb +4 -0
  28. data/lib/etna/directed_graph.rb +88 -5
  29. data/lib/etna/filesystem.rb +143 -15
  30. data/lib/etna/hmac.rb +2 -2
  31. data/lib/etna/route.rb +4 -0
  32. data/lib/etna/spec/auth.rb +6 -6
  33. data/lib/etna/user.rb +15 -11
  34. data/lib/helpers.rb +2 -2
  35. metadata +18 -2
@@ -1,107 +1,71 @@
1
1
  require 'ostruct'
2
- require 'digest'
3
2
  require 'fileutils'
4
3
  require 'tempfile'
5
4
 
6
5
  module Etna
7
6
  module Clients
8
7
  class Metis
9
- class SyncMetisDataWorkflow < Struct.new(:metis_client, :filesystem, :project_name, :bucket_name,
10
- :logger, :skip_tmpdir, keyword_init: true)
11
- def copy_directory(src, dest, root = dest, tmpdir = nil)
12
- own_tmpdir = tmpdir.nil? && !skip_tmpdir
13
- if own_tmpdir
14
- tmpdir = filesystem.tmpdir
15
- end
16
-
17
- begin
18
- response = metis_client.list_folder(ListFolderRequest.new(project_name: project_name, bucket_name: bucket_name, folder_path: src))
8
+ class SyncMetisDataWorkflow < Struct.new(:metis_client, :filesystem, :project_name, :bucket_name, :logger, keyword_init: true)
9
+ def copy_directory(src, dest, root = dest)
10
+ response = metis_client.list_folder(ListFolderRequest.new(project_name: project_name, bucket_name: bucket_name, folder_path: src))
19
11
 
20
- response.files.all.each do |file|
21
- logger&.info("Copying file #{file.file_path} (#{Etna::Formatting.as_size(file.size)})")
22
- copy_file(bin_root_dir: root, tmpdir: tmpdir, dest: ::File.join(dest, file.file_name), url: file.download_url)
23
- end
24
-
25
- response.folders.all.each do |folder|
26
- copy_directory(::File.join(src, folder.folder_name), ::File.join(dest, folder.folder_name), root, tmpdir)
27
- end
28
- ensure
29
- filesystem.rm_rf(tmpdir) if own_tmpdir
12
+ response.files.all.each do |file|
13
+ logger&.info("Copying file #{file.file_path} (#{Etna::Formatting.as_size(file.size)})")
14
+ copy_file(dest: ::File.join(dest, file.file_name), url: file.download_url)
30
15
  end
31
- end
32
-
33
- def bin_file_name(etag:)
34
- "bin/#{etag}"
35
- end
36
16
 
37
- def with_maybe_intermediate_tmp_dest(bin_file_name:, tmpdir:, dest_file_name:, &block)
38
- filesystem.mkdir_p(::File.dirname(dest_file_name))
39
- if tmpdir.nil?
40
- yield dest_file_name
41
- else
42
- tmp_file = ::File.join(tmpdir, ::File.basename(bin_file_name))
43
- yield tmp_file
44
- filesystem.mv(tmp_file, dest_file_name)
17
+ response.folders.all.each do |folder|
18
+ copy_directory(::File.join(src, folder.folder_name), ::File.join(dest, folder.folder_name), root)
45
19
  end
46
20
  end
47
21
 
48
- def copy_file(bin_root_dir:, tmpdir:, dest:, url:, stub: false)
22
+ def copy_file(dest:, url:, stub: false)
49
23
  metadata = metis_client.file_metadata(url)
50
- etag = metadata[:etag]
51
24
  size = metadata[:size]
52
25
 
53
- dest_bin_file = ::File.join(bin_root_dir, bin_file_name(etag: etag))
54
- # Already materialized, continue
55
- if filesystem.exist?(dest_bin_file)
56
- return
57
- end
58
-
59
- with_maybe_intermediate_tmp_dest(bin_file_name: dest_bin_file, tmpdir: tmpdir, dest_file_name: dest) do |tmp_file|
60
- upload_timings = []
61
- upload_amount = 0
62
- last_rate = 0.00001
63
-
64
- filesystem.with_writeable(tmp_file, "w", size_hint: size) do |io|
65
- if stub
66
- io.write("(stub) #{size} bytes")
67
- else
68
- metis_client.download_file(url) do |chunk|
69
- io.write(chunk)
70
-
71
- upload_timings << [chunk.length, Time.now.to_f]
72
- upload_amount += chunk.length
73
-
74
- if upload_timings.length > 150
75
- s, _ = upload_timings.shift
76
- upload_amount -= s
77
- end
26
+ tmp_file = dest
27
+ upload_timings = []
28
+ upload_amount = 0
29
+ last_rate = 0.00001
30
+ remaining = size
31
+
32
+ filesystem.with_writeable(tmp_file, "w", size_hint: size) do |io|
33
+ if stub
34
+ io.write("(stub) #{size} bytes")
35
+ else
36
+ metis_client.download_file(url) do |chunk|
37
+ io.write(chunk)
38
+
39
+ upload_timings << [chunk.length, Time.now.to_f]
40
+ upload_amount += chunk.length
41
+ remaining -= chunk.length
42
+
43
+ if upload_timings.length > 150
44
+ s, _ = upload_timings.shift
45
+ upload_amount -= s
46
+ end
78
47
 
79
- _, start_time = upload_timings.first
80
- _, end_time = upload_timings.last
48
+ _, start_time = upload_timings.first
49
+ _, end_time = upload_timings.last
81
50
 
82
- if start_time == end_time
83
- next
84
- end
51
+ if start_time == end_time
52
+ next
53
+ end
85
54
 
86
- rate = upload_amount / (end_time - start_time)
55
+ rate = upload_amount / (end_time - start_time)
87
56
 
88
- if rate / last_rate > 1.3 || rate / last_rate < 0.7
89
- logger&.info("Uploading #{Etna::Formatting.as_size(rate)} per second")
57
+ if rate / last_rate > 1.3 || rate / last_rate < 0.7
58
+ logger&.info("Uploading #{Etna::Formatting.as_size(rate)} per second, #{Etna::Formatting.as_size(remaining)} remaining")
90
59
 
91
- if rate == 0
92
- last_rate = 0.0001
93
- else
94
- last_rate = rate
95
- end
60
+ if rate == 0
61
+ last_rate = 0.0001
62
+ else
63
+ last_rate = rate
96
64
  end
97
65
  end
98
66
  end
99
- end
100
- end
101
67
 
102
- filesystem.mkdir_p(::File.dirname(dest_bin_file))
103
- filesystem.with_writeable(dest_bin_file, 'w', size_hint: 0) do |io|
104
- # empty file marking that this etag has been moved, to save a future write.
68
+ end
105
69
  end
106
70
  end
107
71
  end
data/lib/etna/command.rb CHANGED
@@ -203,6 +203,7 @@ module Etna
203
203
  @subcommands ||= self.class.constants.sort.reduce({}) do |acc, n|
204
204
  acc.tap do
205
205
  c = self.class.const_get(n)
206
+ next unless c.respond_to?(:instance_methods)
206
207
  next unless c.instance_methods.include?(:find_command)
207
208
  v = c.new(self)
208
209
  acc[v.command_name] = v
data/lib/etna/cwl.rb CHANGED
@@ -584,6 +584,10 @@ module Etna
584
584
  def default
585
585
  @attributes['default']
586
586
  end
587
+
588
+ def type
589
+ @attributes['type']
590
+ end
587
591
  end
588
592
 
589
593
  class StepOutput < Cwl
@@ -7,6 +7,59 @@ class DirectedGraph
7
7
  attr_reader :children
8
8
  attr_reader :parents
9
9
 
10
+ def full_parentage(n)
11
+ [].tap do |result|
12
+ q = @parents[n].keys.dup
13
+ seen = Set.new
14
+
15
+ until q.empty?
16
+ n = q.shift
17
+ next if seen.include?(n)
18
+ seen.add(n)
19
+
20
+ result << n
21
+ q.push(*@parents[n].keys)
22
+ end
23
+
24
+ result.uniq!
25
+ end
26
+ end
27
+
28
+ def as_normalized_hash(root, include_root = true)
29
+ q = [root]
30
+ {}.tap do |result|
31
+ if include_root
32
+ result[root] = []
33
+ end
34
+
35
+ seen = Set.new
36
+
37
+ until q.empty?
38
+ n = q.shift
39
+ next if seen.include?(n)
40
+ seen.add(n)
41
+
42
+ parentage = full_parentage(n)
43
+
44
+ @children[n].keys.each do |child_node|
45
+ q << child_node
46
+
47
+ if result.include?(n)
48
+ result[n] << child_node
49
+ end
50
+
51
+ parentage.each do |grandparent|
52
+ result[grandparent] << child_node if result.include?(grandparent)
53
+ end
54
+
55
+ result[child_node] = []
56
+ end
57
+ end
58
+
59
+ result.values.each(&:uniq!)
60
+ end
61
+ end
62
+
10
63
  def add_connection(parent, child)
11
64
  children = @children[parent] ||= {}
12
65
  child_children = @children[child] ||= {}
@@ -18,12 +71,42 @@ class DirectedGraph
18
71
  parents[parent] = parent_parents
19
72
  end
20
73
 
21
- def paths_from(root)
74
+ def serialized_path_from(root, include_root = true)
75
+ seen = Set.new
76
+ [].tap do |result|
77
+ result << root if include_root
78
+ seen.add(root)
79
+ path_q = paths_from(root, include_root)
80
+ traversables = path_q.flatten
81
+
82
+ until path_q.empty?
83
+ next_path = path_q.shift
84
+ next if next_path.nil?
85
+
86
+ until next_path.empty?
87
+ next_n = next_path.shift
88
+ next if next_n.nil?
89
+ next if seen.include?(next_n)
90
+
91
+ if @parents[next_n].keys.any? { |p| !seen.include?(p) && traversables.include?(p) }
92
+ next_path.unshift(next_n)
93
+ path_q.push(next_path)
94
+ break
95
+ else
96
+ result << next_n
97
+ seen.add(next_n)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ def paths_from(root, include_root = true)
22
105
  [].tap do |result|
23
- parents_of_map = descendants(root)
106
+ parents_of_map = descendants(root, include_root)
24
107
  seen = Set.new
25
108
 
26
- parents_of_map.to_a.sort_by { |k, parents| [-parents.length, k] }.each do |k, parents|
109
+ parents_of_map.to_a.sort_by { |k, parents| [-parents.length, k.inspect] }.each do |k, parents|
27
110
  unless seen.include?(k)
28
111
  if @children[k].keys.empty?
29
112
  result << parents + [k]
@@ -39,7 +122,7 @@ class DirectedGraph
39
122
  end
40
123
  end
41
124
 
42
- def descendants(parent)
125
+ def descendants(parent, include_root = true)
43
126
  seen = Set.new
44
127
 
45
128
  seen.add(parent)
@@ -61,7 +144,7 @@ class DirectedGraph
61
144
  while child = queue.pop
62
145
  next if seen.include? child
63
146
  seen.add(child)
64
- path = (paths[child] ||= [parent])
147
+ path = (paths[child] ||= (include_root ? [parent] : []))
65
148
 
66
149
  @children[child].keys.each do |child_child|
67
150
  queue.push child_child
@@ -1,36 +1,52 @@
1
1
  require 'yaml'
2
2
  require 'fileutils'
3
3
  require 'open3'
4
+ require 'securerandom'
5
+ require 'concurrent-ruby'
4
6
 
5
7
  module Etna
6
8
  # A class that encapsulates opening / reading file system entries that abstracts normal file access in order
7
9
  # to make stubbing, substituting, and testing easier.
8
10
  class Filesystem
9
11
  def with_writeable(dest, opts = 'w', size_hint: nil, &block)
12
+ raise "with_writeable not supported by #{self.class.name}" unless self.class == Filesystem
10
13
  ::File.open(dest, opts, &block)
11
14
  end
12
15
 
16
+ def ls(dir)
17
+ raise "ls not supported by #{self.class.name}" unless self.class == Filesystem
18
+ ::Dir.entries(dir).select { |p| !p.start_with?('.') }.map do |path|
19
+ ::File.file?(::File.join(dir, path)) ? [:file, path] : [:dir, path]
20
+ end
21
+ end
22
+
13
23
  def with_readable(src, opts = 'r', &block)
24
+ raise "with_readable not supported by #{self.class.name}" unless self.class == Filesystem
14
25
  ::File.open(src, opts, &block)
15
26
  end
16
27
 
17
28
  def mkdir_p(dir)
29
+ raise "mkdir_p not supported by #{self.class.name}" unless self.class == Filesystem
18
30
  ::FileUtils.mkdir_p(dir)
19
31
  end
20
32
 
21
33
  def rm_rf(dir)
34
+ raise "rm_rf not supported by #{self.class.name}" unless self.class == Filesystem
22
35
  ::FileUtils.rm_rf(dir)
23
36
  end
24
37
 
25
38
  def tmpdir
39
+ raise "tmpdir not supported by #{self.class.name}" unless self.class == Filesystem
26
40
  ::Dir.mktmpdir
27
41
  end
28
42
 
29
43
  def exist?(src)
44
+ raise "exist? not supported by #{self.class.name}" unless self.class == Filesystem
30
45
  ::File.exist?(src)
31
46
  end
32
47
 
33
48
  def mv(src, dest)
49
+ raise "mv not supported by #{self.class.name}" unless self.class == Filesystem
34
50
  ::FileUtils.mv(src, dest)
35
51
  end
36
52
 
@@ -179,7 +195,7 @@ module Etna
179
195
  cmd << remote_path
180
196
  cmd << local_path
181
197
 
182
- cmd << { out: wd }
198
+ cmd << {out: wd}
183
199
  elsif opts.include?('w')
184
200
  cmd << '--mode=send'
185
201
  cmd << "--host=#{@host}"
@@ -187,7 +203,7 @@ module Etna
187
203
  cmd << local_path
188
204
  cmd << remote_path
189
205
 
190
- cmd << { in: rd }
206
+ cmd << {in: rd}
191
207
  end
192
208
 
193
209
  cmd
@@ -196,23 +212,10 @@ module Etna
196
212
 
197
213
  # Genentech's aspera deployment doesn't support modern commands, unfortunately...
198
214
  class GeneAsperaCliFilesystem < AsperaCliFilesystem
199
- def tmpdir
200
- raise "tmpdir is not supported"
201
- end
202
-
203
- def rm_rf
204
- raise "rm_rf is not supported"
205
- end
206
-
207
215
  def mkdir_p(dest)
208
216
  # Pass through -- this file system creates containing directories by default, womp womp.
209
217
  end
210
218
 
211
- def mv
212
- raise "mv is not supported"
213
- end
214
-
215
-
216
219
  def mkcommand(rd, wd, file, opts, size_hint: nil)
217
220
  if opts.include?('w')
218
221
  super.map do |e|
@@ -241,6 +244,131 @@ module Etna
241
244
  end
242
245
  end
243
246
 
247
+ class Metis < Filesystem
248
+ def initialize(metis_client:, project_name:, bucket_name:, root: '/', uuid: SecureRandom.uuid)
249
+ @metis_client = metis_client
250
+ @project_name = project_name
251
+ @bucket_name = bucket_name
252
+ @root = root
253
+ @metis_uid = uuid
254
+ end
255
+
256
+ def metis_path_of(path)
257
+ joined = ::File.join(@root, path)
258
+ joined[0] == "/" ? joined.slice(1..-1) : joined
259
+ end
260
+
261
+ def create_upload_workflow
262
+ Etna::Clients::Metis::MetisUploadWorkflow.new(metis_client: @metis_client, metis_uid: @metis_uid, project_name: @project_name, bucket_name: @bucket_name, max_attempts: 3)
263
+ end
264
+
265
+ def with_hot_pipe(opts, receiver, *args, &block)
266
+ rp, wp = IO.pipe
267
+ begin
268
+ executor = Concurrent::SingleThreadExecutor.new(fallback_policy: :abort)
269
+ begin
270
+ if opts.include?('w')
271
+ future = Concurrent::Promises.future_on(executor) do
272
+ self.send(receiver, rp, *args)
273
+ rescue => e
274
+ Etna::Application.instance.logger.log_error(e)
275
+ raise e
276
+ ensure
277
+ rp.close
278
+ end
279
+
280
+ yield wp
281
+ else
282
+ future = Concurrent::Promises.future_on(executor) do
283
+ self.send(receiver, wp, *args)
284
+ rescue => e
285
+ Etna::Application.instance.logger.log_error(e)
286
+ raise e
287
+ ensure
288
+ wp.close
289
+ end
290
+
291
+ yield rp
292
+ end
293
+
294
+ future.wait!
295
+ ensure
296
+ executor.shutdown
297
+ executor.kill unless executor.wait_for_termination(5)
298
+ end
299
+ ensure
300
+ rp.close
301
+ wp.close
302
+ end
303
+ end
304
+
305
+ def do_streaming_upload(rp, dest, size_hint)
306
+ streaming_upload = Etna::Clients::Metis::MetisUploadWorkflow::StreamingIOUpload.new(readable_io: rp, size_hint: size_hint)
307
+ create_upload_workflow.do_upload(
308
+ streaming_upload,
309
+ metis_path_of(dest)
310
+ )
311
+ end
312
+
313
+ def with_writeable(dest, opts = 'w', size_hint: nil, &block)
314
+ self.with_hot_pipe(opts, :do_streaming_upload, dest, size_hint) do |wp|
315
+ yield wp
316
+ end
317
+ end
318
+
319
+ def create_download_workflow
320
+ Etna::Clients::Metis::MetisDownloadWorkflow.new(metis_client: @metis_client, project_name: @project_name, bucket_name: @bucket_name, max_attempts: 3)
321
+ end
322
+
323
+ def do_streaming_download(wp, metis_file)
324
+ create_download_workflow.do_download(wp, metis_file)
325
+ end
326
+
327
+ def with_readable(src, opts = 'r', &block)
328
+ metis_file = list_metis_directory(::File.dirname(src)).files.all.find { |f| f.file_name == ::File.basename(src) }
329
+ raise "Metis file at #{@project_name}/#{@bucket_name}/#{@root}/#{src} not found. No such file" if metis_file.nil?
330
+
331
+ self.with_hot_pipe(opts, :do_streaming_download, metis_file) do |rp|
332
+ yield rp
333
+ end
334
+ end
335
+
336
+ def list_metis_directory(path)
337
+ @metis_client.list_folder(Etna::Clients::Metis::ListFolderRequest.new(project_name: @project_name, bucket_name: @bucket_name, folder_path: metis_path_of(path)))
338
+ end
339
+
340
+ def mkdir_p(dir)
341
+ create_folder_request = Etna::Clients::Metis::CreateFolderRequest.new(
342
+ project_name: @project_name,
343
+ bucket_name: @bucket_name,
344
+ folder_path: metis_path_of(dir),
345
+ )
346
+ @metis_client.create_folder(create_folder_request)
347
+ end
348
+
349
+ def ls(dir)
350
+ response = list_metis_directory(::File.dirname(dir))
351
+ response.files.map { |f| [:file, f.file_name] } + response.folders.map { |f| [:folder, f.folder_name] }
352
+ end
353
+
354
+ def exist?(src)
355
+ begin
356
+ response = list_metis_directory(::File.dirname(src))
357
+ rescue Etna::Error => e
358
+ if e.status == 404
359
+ return false
360
+ elsif e.message =~ /Invalid folder/
361
+ return false
362
+ end
363
+
364
+ raise e
365
+ end
366
+
367
+ response.files.all.any? { |f| f.file_name == ::File.basename(src) } ||
368
+ response.folders.all.any? { |f| f.folder_name == ::File.basename(src) }
369
+ end
370
+ end
371
+
244
372
  class Mock < Filesystem
245
373
  def initialize(&new_io)
246
374
  @files = {}