etna 0.1.27 → 0.1.32
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/etna.completion +137 -55
- data/lib/etna.rb +1 -0
- data/lib/etna/application.rb +4 -0
- data/lib/etna/client.rb +30 -2
- data/lib/etna/clients/magma/formatting.rb +2 -1
- data/lib/etna/clients/magma/formatting/models_odm_xml.rb +293 -0
- data/lib/etna/clients/magma/models.rb +13 -1
- data/lib/etna/clients/magma/workflows/create_project_workflow.rb +1 -1
- data/lib/etna/clients/magma/workflows/crud_workflow.rb +19 -2
- data/lib/etna/clients/magma/workflows/materialize_magma_record_files_workflow.rb +46 -30
- data/lib/etna/clients/magma/workflows/model_synchronization_workflow.rb +1 -1
- data/lib/etna/clients/magma/workflows/update_attributes_from_csv_workflow.rb +19 -6
- data/lib/etna/clients/magma/workflows/walk_model_tree_workflow.rb +33 -6
- data/lib/etna/clients/metis/client.rb +6 -1
- data/lib/etna/clients/metis/models.rb +15 -0
- data/lib/etna/clients/metis/workflows/metis_download_workflow.rb +15 -11
- data/lib/etna/clients/metis/workflows/metis_upload_workflow.rb +83 -13
- data/lib/etna/clients/metis/workflows/sync_metis_data_workflow.rb +43 -79
- data/lib/etna/command.rb +10 -0
- data/lib/etna/cwl.rb +701 -0
- data/lib/etna/directed_graph.rb +50 -0
- data/lib/etna/filesystem.rb +145 -15
- data/lib/etna/generate_autocompletion_script.rb +11 -6
- data/lib/etna/hmac.rb +2 -2
- data/lib/etna/route.rb +44 -4
- data/lib/etna/spec/auth.rb +6 -6
- data/lib/etna/user.rb +15 -11
- metadata +33 -3
data/lib/etna/directed_graph.rb
CHANGED
@@ -18,6 +18,56 @@ class DirectedGraph
|
|
18
18
|
parents[parent] = parent_parents
|
19
19
|
end
|
20
20
|
|
21
|
+
def serialized_path_from(root)
|
22
|
+
seen = Set.new
|
23
|
+
[].tap do |result|
|
24
|
+
result << root
|
25
|
+
seen.add(root)
|
26
|
+
path_q = paths_from(root)
|
27
|
+
|
28
|
+
until path_q.empty?
|
29
|
+
next_path = path_q.shift
|
30
|
+
next if next_path.nil?
|
31
|
+
|
32
|
+
until next_path.empty?
|
33
|
+
next_n = next_path.shift
|
34
|
+
next if next_n.nil?
|
35
|
+
next if seen.include?(next_n)
|
36
|
+
|
37
|
+
if @parents[next_n].keys.any? { |p| !seen.include?(p) }
|
38
|
+
next_path.unshift(next_n)
|
39
|
+
path_q.push(next_path)
|
40
|
+
break
|
41
|
+
else
|
42
|
+
result << next_n
|
43
|
+
seen.add(next_n)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def paths_from(root)
|
51
|
+
[].tap do |result|
|
52
|
+
parents_of_map = descendants(root)
|
53
|
+
seen = Set.new
|
54
|
+
|
55
|
+
parents_of_map.to_a.sort_by { |k, parents| [-parents.length, k.inspect] }.each do |k, parents|
|
56
|
+
unless seen.include?(k)
|
57
|
+
if @children[k].keys.empty?
|
58
|
+
result << parents + [k]
|
59
|
+
else
|
60
|
+
@children[k].keys.dup.sort.each do |c|
|
61
|
+
result << parents + [k, c]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
parents.each { |p| seen.add(p) }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
21
71
|
def descendants(parent)
|
22
72
|
seen = Set.new
|
23
73
|
|
data/lib/etna/filesystem.rb
CHANGED
@@ -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 << {
|
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 << {
|
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|
|
@@ -230,6 +233,8 @@ module Etna
|
|
230
233
|
end
|
231
234
|
|
232
235
|
def with_writeable(dest, opts = 'w', size_hint: nil, &block)
|
236
|
+
raise "#{self.class.name} requires size_hint in with_writeable" if size_hint.nil?
|
237
|
+
|
233
238
|
super do |io|
|
234
239
|
io.write("File: #{::File.basename(dest)}\n")
|
235
240
|
io.write("Size: #{size_hint}\n")
|
@@ -239,6 +244,131 @@ module Etna
|
|
239
244
|
end
|
240
245
|
end
|
241
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
|
+
|
242
372
|
class Mock < Filesystem
|
243
373
|
def initialize(&new_io)
|
244
374
|
@files = {}
|
@@ -29,10 +29,12 @@ module Etna
|
|
29
29
|
write %Q(elif [[ -z "$(echo $all_flag_completion_names | xargs)" ]]; then)
|
30
30
|
write "return"
|
31
31
|
write %Q(elif [[ "$all_flag_completion_names" =~ $1\\ ]]; then)
|
32
|
+
write %Q(if ! [[ "$multi_flags" =~ $1\\ ]]; then)
|
32
33
|
write %Q(all_flag_completion_names="${all_flag_completion_names//$1\\ /}")
|
34
|
+
write 'fi'
|
33
35
|
write 'a=$1'
|
34
36
|
write 'shift'
|
35
|
-
write %Q(if [[ "$
|
37
|
+
write %Q(if [[ "$arg_flag_completion_names" =~ $a\\ ]]; then)
|
36
38
|
write 'if [[ "$#" == "1" ]]; then'
|
37
39
|
write %Q(a="${a//--/}")
|
38
40
|
write %Q(a="${a//-/_}")
|
@@ -58,12 +60,14 @@ module Etna
|
|
58
60
|
|
59
61
|
def enable_flags(flags_container)
|
60
62
|
boolean_flags = flags_container.boolean_flags
|
61
|
-
|
62
|
-
|
63
|
+
multi_flags = flags_container.multi_flags
|
64
|
+
arg_flags = flags_container.string_flags + multi_flags
|
65
|
+
flags = boolean_flags + arg_flags
|
63
66
|
write %Q(all_flag_completion_names="$all_flag_completion_names #{flags.join(' ')} ")
|
64
|
-
write %Q(
|
67
|
+
write %Q(arg_flag_completion_names="$arg_flag_completion_names #{arg_flags.join(' ')} ")
|
68
|
+
write %Q(multi_flags="$multi_flags #{multi_flags.join(' ')} ")
|
65
69
|
|
66
|
-
|
70
|
+
arg_flags.each do |flag|
|
67
71
|
write %Q(declare _completions_for_#{flag_as_parameter(flag)}="#{completions_for(flag_as_parameter(flag)).join(' ')}")
|
68
72
|
end
|
69
73
|
end
|
@@ -107,7 +111,8 @@ function _#{name}_completions() {
|
|
107
111
|
|
108
112
|
function _#{name}_inner_completions() {
|
109
113
|
local all_flag_completion_names=''
|
110
|
-
local
|
114
|
+
local arg_flag_completion_names=''
|
115
|
+
local multi_flags=''
|
111
116
|
local all_completion_names=''
|
112
117
|
local i=''
|
113
118
|
local a=''
|
data/lib/etna/hmac.rb
CHANGED
@@ -20,14 +20,14 @@ module Etna
|
|
20
20
|
end
|
21
21
|
|
22
22
|
# this returns arguments for URI::HTTP.build
|
23
|
-
def url_params
|
23
|
+
def url_params(with_headers=true)
|
24
24
|
params = {
|
25
25
|
signature: signature,
|
26
26
|
expiration: @expiration,
|
27
27
|
nonce: @nonce,
|
28
28
|
id: @id.to_s,
|
29
29
|
headers: @headers.keys.join(','),
|
30
|
-
}.merge(@headers).map do |name, value|
|
30
|
+
}.merge(with_headers ? @headers : {}).map do |name, value|
|
31
31
|
[
|
32
32
|
"X-Etna-#{ name.to_s.split(/_/).map(&:capitalize).join('-') }",
|
33
33
|
value
|
data/lib/etna/route.rb
CHANGED
@@ -9,6 +9,7 @@ module Etna
|
|
9
9
|
@name = route_name(options)
|
10
10
|
@route = route.gsub(/\A(?=[^\/])/, '/')
|
11
11
|
@block = block
|
12
|
+
@match_ext = options[:match_ext]
|
12
13
|
end
|
13
14
|
|
14
15
|
def to_hash
|
@@ -72,9 +73,7 @@ module Etna
|
|
72
73
|
user = request.env['etna.user']
|
73
74
|
|
74
75
|
params = request.env['rack.request.params'].map do |key,value|
|
75
|
-
|
76
|
-
value = value[0..500] + "..." + value[-100..-1] if value.length > 600
|
77
|
-
[ key, value ]
|
76
|
+
[ key, redact(key, value) ]
|
78
77
|
end.to_h
|
79
78
|
|
80
79
|
logger.warn("User #{user ? user.email : :unknown} calling #{controller}##{action} with params #{params}")
|
@@ -94,6 +93,39 @@ module Etna
|
|
94
93
|
|
95
94
|
private
|
96
95
|
|
96
|
+
def application
|
97
|
+
@application ||= Etna::Application.instance
|
98
|
+
end
|
99
|
+
|
100
|
+
def compact(value)
|
101
|
+
value = value.to_s
|
102
|
+
value = value[0..500] + "..." + value[-100..-1] if value.length > 600
|
103
|
+
value
|
104
|
+
end
|
105
|
+
|
106
|
+
def redact_keys
|
107
|
+
@redact_keys ||= application.config(:log_redact_keys).split(",").map do |key|
|
108
|
+
key.to_sym
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def redact(key, value)
|
113
|
+
# From configuration, redact any values for the supplied key values, so they
|
114
|
+
# don't appear in the logs.
|
115
|
+
return compact(value) unless application.config(:log_redact_keys)
|
116
|
+
|
117
|
+
if value.is_a?(Hash)
|
118
|
+
redacted_value = value.map do |value_key, value_value|
|
119
|
+
[ value_key, redact(value_key, value_value) ]
|
120
|
+
end.to_h
|
121
|
+
return redacted_value
|
122
|
+
elsif redact_keys.include?(key)
|
123
|
+
return "*"
|
124
|
+
end
|
125
|
+
|
126
|
+
return compact(value)
|
127
|
+
end
|
128
|
+
|
97
129
|
def authorized?(request)
|
98
130
|
# If there is no @auth requirement, they are ok - this doesn't preclude
|
99
131
|
# them being rejected in the controller response
|
@@ -145,13 +177,21 @@ module Etna
|
|
145
177
|
)
|
146
178
|
end
|
147
179
|
|
180
|
+
def separator_free_match
|
181
|
+
if @match_ext
|
182
|
+
'(?<\1>[^\/\?]+)'
|
183
|
+
else
|
184
|
+
'(?<\1>[^\.\/\?]+)'
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
148
188
|
def route_regexp
|
149
189
|
@route_regexp ||=
|
150
190
|
Regexp.new(
|
151
191
|
'\A' +
|
152
192
|
@route.
|
153
193
|
# any :params match separator-free strings
|
154
|
-
gsub(NAMED_PARAM,
|
194
|
+
gsub(NAMED_PARAM, separator_free_match).
|
155
195
|
# any *params match arbitrary strings
|
156
196
|
gsub(GLOB_PARAM, '(?<\1>.+)').
|
157
197
|
# ignore any trailing slashes in the route
|
data/lib/etna/spec/auth.rb
CHANGED
@@ -2,22 +2,22 @@ module Etna::Spec
|
|
2
2
|
module Auth
|
3
3
|
AUTH_USERS = {
|
4
4
|
superuser: {
|
5
|
-
email: 'zeus@olympus.org',
|
5
|
+
email: 'zeus@olympus.org', name: 'Zeus', perm: 'A:administration'
|
6
6
|
},
|
7
7
|
admin: {
|
8
|
-
email: 'hera@olympus.org',
|
8
|
+
email: 'hera@olympus.org', name: 'Hera', perm: 'a:labors'
|
9
9
|
},
|
10
10
|
editor: {
|
11
|
-
email: 'eurystheus@twelve-labors.org',
|
11
|
+
email: 'eurystheus@twelve-labors.org', name: 'Eurystheus', perm: 'E:labors'
|
12
12
|
},
|
13
13
|
restricted_editor: {
|
14
|
-
email: 'copreus@twelve-labors.org',
|
14
|
+
email: 'copreus@twelve-labors.org', name: 'Copreus', perm: 'e:labors'
|
15
15
|
},
|
16
16
|
viewer: {
|
17
|
-
email: 'hercules@twelve-labors.org',
|
17
|
+
email: 'hercules@twelve-labors.org', name: 'Hercules', perm: 'v:labors'
|
18
18
|
},
|
19
19
|
non_user: {
|
20
|
-
email: 'nessus@centaurs.org',
|
20
|
+
email: 'nessus@centaurs.org', name: 'Nessus', perm: ''
|
21
21
|
}
|
22
22
|
}
|
23
23
|
|