etna 0.1.20 → 0.1.25
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 +26926 -2
- data/lib/commands.rb +82 -3
- data/lib/etna.rb +1 -0
- data/lib/etna/client.rb +15 -36
- data/lib/etna/clients/base_client.rb +39 -0
- data/lib/etna/clients/janus/client.rb +2 -13
- data/lib/etna/clients/janus/models.rb +2 -1
- data/lib/etna/clients/magma/client.rb +2 -17
- data/lib/etna/clients/magma/models.rb +7 -3
- data/lib/etna/clients/magma/workflows.rb +2 -0
- data/lib/etna/clients/magma/workflows/crud_workflow.rb +10 -4
- data/lib/etna/clients/magma/workflows/materialize_magma_record_files_workflow.rb +135 -0
- data/lib/etna/clients/magma/workflows/walk_model_tree_workflow.rb +101 -0
- data/lib/etna/clients/metis/client.rb +22 -9
- data/lib/etna/clients/metis/models.rb +3 -2
- data/lib/etna/clients/polyphemus/client.rb +18 -13
- data/lib/etna/clients/polyphemus/models.rb +26 -1
- data/lib/etna/csvs.rb +2 -1
- data/lib/etna/filesystem.rb +93 -0
- data/lib/etna/spec/vcr.rb +7 -0
- data/lib/etna/test_auth.rb +7 -3
- data/lib/helpers.rb +9 -3
- metadata +6 -2
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'digest'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'tempfile'
|
5
|
+
|
6
|
+
module Etna
|
7
|
+
module Clients
|
8
|
+
class Magma
|
9
|
+
class WalkModelTreeWorkflow < Struct.new(:magma_crud, :logger, keyword_init: true)
|
10
|
+
def walk_from(
|
11
|
+
model_name,
|
12
|
+
record_names = 'all',
|
13
|
+
model_attributes_mask: {},
|
14
|
+
model_filters: {},
|
15
|
+
page_size: 100,
|
16
|
+
&block)
|
17
|
+
q = [ { model_name: model_name, from: nil, record_names: record_names } ]
|
18
|
+
seen = Set.new
|
19
|
+
|
20
|
+
while (path = q.pop)
|
21
|
+
model_name = path[:model_name]
|
22
|
+
next if seen.include?([path[:from], model_name])
|
23
|
+
seen.add([path[:from], model_name])
|
24
|
+
|
25
|
+
request = RetrievalRequest.new(
|
26
|
+
project_name: magma_crud.project_name,
|
27
|
+
model_name: model_name,
|
28
|
+
record_names: path[:record_names],
|
29
|
+
filter: model_filters[model_name],
|
30
|
+
page_size: page_size, page: 1
|
31
|
+
)
|
32
|
+
|
33
|
+
related_models = {}
|
34
|
+
|
35
|
+
magma_crud.page_records(model_name, request) do |response|
|
36
|
+
model = response.models.model(model_name)
|
37
|
+
template = model.template
|
38
|
+
|
39
|
+
tables = []
|
40
|
+
collections = []
|
41
|
+
links = []
|
42
|
+
attributes = []
|
43
|
+
|
44
|
+
template.attributes.attribute_keys.each do |attr_name|
|
45
|
+
attributes_mask = model_attributes_mask[model_name]
|
46
|
+
next if !attributes_mask.nil? && !attributes_mask.include?(attr_name) && attr_name != template.identifier
|
47
|
+
attributes << attr_name
|
48
|
+
|
49
|
+
attr = template.attributes.attribute(attr_name)
|
50
|
+
if attr.attribute_type == AttributeType::TABLE
|
51
|
+
tables << attr_name
|
52
|
+
elsif attr.attribute_type == AttributeType::COLLECTION
|
53
|
+
related_models[attr.link_model_name] ||= Set.new
|
54
|
+
collections << attr_name
|
55
|
+
elsif attr.attribute_type == AttributeType::LINK
|
56
|
+
related_models[attr.link_model_name] ||= Set.new
|
57
|
+
links << attr_name
|
58
|
+
elsif attr.attribute_type == AttributeType::CHILD
|
59
|
+
related_models[attr.link_model_name] ||= Set.new
|
60
|
+
links << attr_name
|
61
|
+
elsif attr.attribute_type == AttributeType::PARENT
|
62
|
+
related_models[attr.link_model_name] ||= Set.new
|
63
|
+
links << attr_name
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
model.documents.document_keys.each do |key|
|
68
|
+
record = model.documents.document(key).slice(*attributes)
|
69
|
+
|
70
|
+
# Inline tables inside the record
|
71
|
+
tables.each do |table_attr|
|
72
|
+
record[table_attr] = record[table_attr].map do |id|
|
73
|
+
response.models.model(template.attributes.attribute(table_attr).link_model_name).documents.document(id)
|
74
|
+
end unless record[table_attr].nil?
|
75
|
+
end
|
76
|
+
|
77
|
+
collections.each do |collection_attr|
|
78
|
+
record[collection_attr].each do |collected_id|
|
79
|
+
related_models[template.attributes.attribute(collection_attr).link_model_name].add(collected_id)
|
80
|
+
end unless record[collection_attr].nil?
|
81
|
+
end
|
82
|
+
|
83
|
+
links.each do |link_attr|
|
84
|
+
related_models[template.attributes.attribute(link_attr).link_model_name].add(record[link_attr]) unless record[link_attr].nil?
|
85
|
+
end
|
86
|
+
|
87
|
+
yield template, record
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
related_models.each do |link_model_name, id_set|
|
92
|
+
next if id_set.empty?
|
93
|
+
q.push({ model_name: link_model_name, from: model_name, record_names: id_set.to_a })
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
@@ -1,23 +1,19 @@
|
|
1
|
-
require 'net/http/persistent'
|
2
1
|
require 'net/http/post/multipart'
|
3
2
|
require 'singleton'
|
4
3
|
require 'cgi'
|
5
4
|
require 'json'
|
6
5
|
require_relative '../../client'
|
7
6
|
require_relative './models'
|
7
|
+
require_relative '../base_client'
|
8
8
|
|
9
9
|
module Etna
|
10
10
|
module Clients
|
11
|
-
class Metis
|
12
|
-
|
13
|
-
def initialize(host:, token:,
|
11
|
+
class Metis < Etna::Clients::BaseClient
|
12
|
+
|
13
|
+
def initialize(host:, token:, ignore_ssl: false)
|
14
14
|
raise 'Metis client configuration is missing host.' unless host
|
15
15
|
raise 'Metis client configuration is missing token.' unless token
|
16
|
-
@etna_client = ::Etna::Client.new(
|
17
|
-
host,
|
18
|
-
token,
|
19
|
-
persistent: persistent,
|
20
|
-
ignore_ssl: ignore_ssl)
|
16
|
+
@etna_client = ::Etna::Client.new(host, token, ignore_ssl: ignore_ssl)
|
21
17
|
|
22
18
|
@token = token
|
23
19
|
end
|
@@ -90,6 +86,23 @@ module Etna
|
|
90
86
|
end
|
91
87
|
end
|
92
88
|
|
89
|
+
def file_metadata(file_or_url = File.new)
|
90
|
+
if file_or_url.instance_of?(File)
|
91
|
+
download_path = file_or_url.download_path
|
92
|
+
else
|
93
|
+
download_path = file_or_url.sub(%r!^https://[^/]*?/!, '/')
|
94
|
+
end
|
95
|
+
|
96
|
+
# Do not actually consume the data, however.
|
97
|
+
# TODO: implement HEAD requests in metis through apache.
|
98
|
+
@etna_client.get(download_path) do |response|
|
99
|
+
return {
|
100
|
+
etag: response['ETag'].gsub(/"/, ''),
|
101
|
+
size: response['Content-Length'],
|
102
|
+
}
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
93
106
|
def upload_start(upload_start_request = UploadStartRequest.new)
|
94
107
|
json = nil
|
95
108
|
@etna_client.post(upload_start_request.upload_path, upload_start_request) do |res|
|
@@ -1,9 +1,10 @@
|
|
1
|
-
require_relative '../../json_serializable_struct'
|
2
1
|
require 'ostruct'
|
2
|
+
require_relative '../../json_serializable_struct'
|
3
|
+
require_relative '../base_client'
|
3
4
|
|
4
5
|
module Etna
|
5
6
|
module Clients
|
6
|
-
class Metis
|
7
|
+
class Metis < Etna::Clients::BaseClient
|
7
8
|
class ListFoldersRequest < Struct.new(:project_name, :bucket_name, :offset, :limit, keyword_init: true)
|
8
9
|
include JsonSerializableStruct
|
9
10
|
|
@@ -1,23 +1,12 @@
|
|
1
|
-
require 'net/http/persistent'
|
2
1
|
require 'net/http/post/multipart'
|
3
2
|
require 'singleton'
|
4
3
|
require_relative '../../client'
|
5
4
|
require_relative './models'
|
5
|
+
require_relative '../base_client'
|
6
6
|
|
7
7
|
module Etna
|
8
8
|
module Clients
|
9
|
-
class Polyphemus
|
10
|
-
def initialize(host:, token:, ignore_ssl: false, persistent: true)
|
11
|
-
raise 'Polyphemus client configuration is missing host.' unless host
|
12
|
-
raise 'Polyphemus client configuration is missing token.' unless token
|
13
|
-
@etna_client = ::Etna::Client.new(
|
14
|
-
host,
|
15
|
-
token,
|
16
|
-
routes_available: false,
|
17
|
-
persistent: persistent,
|
18
|
-
ignore_ssl: ignore_ssl)
|
19
|
-
end
|
20
|
-
|
9
|
+
class Polyphemus < Etna::Clients::BaseClient
|
21
10
|
def configuration(configuration_request = ConfigurationRequest.new)
|
22
11
|
json = nil
|
23
12
|
@etna_client.get(
|
@@ -28,6 +17,22 @@ module Etna
|
|
28
17
|
|
29
18
|
ConfigurationResponse.new(json)
|
30
19
|
end
|
20
|
+
|
21
|
+
def job(job_request = JobRequest.new)
|
22
|
+
# Because this is a streaming response, just yield the response back.
|
23
|
+
# The consumer will have to iterate over the response.read_body, like
|
24
|
+
#
|
25
|
+
# polyphemus_client.job(request) do |response|
|
26
|
+
# response.read_body do |fragment|
|
27
|
+
# <fragment contains a chunk of text streamed back from the server>
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
@etna_client.post(
|
31
|
+
"/#{job_request.project_name}/job",
|
32
|
+
job_request) do |res|
|
33
|
+
yield res
|
34
|
+
end
|
35
|
+
end
|
31
36
|
end
|
32
37
|
end
|
33
38
|
end
|
@@ -1,17 +1,37 @@
|
|
1
1
|
require 'ostruct'
|
2
2
|
require_relative '../../json_serializable_struct'
|
3
|
+
require_relative '../base_client'
|
3
4
|
|
4
5
|
# TODO: In the near future, I'd like to transition to specifying apis via SWAGGER and generating model stubs from the
|
5
6
|
# common definitions. For nowe I've written them out by hand here.
|
6
7
|
module Etna
|
7
8
|
module Clients
|
8
|
-
class Polyphemus
|
9
|
+
class Polyphemus < Etna::Clients::BaseClient
|
9
10
|
class ConfigurationRequest
|
10
11
|
def map
|
11
12
|
[]
|
12
13
|
end
|
13
14
|
end
|
14
15
|
|
16
|
+
class RedcapJobRequest < Struct.new(:model_names, :redcap_tokens, :commit, :project_name, keyword_init: true)
|
17
|
+
include JsonSerializableStruct
|
18
|
+
|
19
|
+
def initialize(**params)
|
20
|
+
super({model_names: 'all', commit: false}.update(params))
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_json
|
24
|
+
{
|
25
|
+
job_type: Etna::Clients::Polyphemus::JobType::REDCAP,
|
26
|
+
job_params: {
|
27
|
+
commit: commit,
|
28
|
+
model_names: model_names,
|
29
|
+
redcap_tokens: redcap_tokens
|
30
|
+
}
|
31
|
+
}.to_json
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
15
35
|
class ConfigurationResponse
|
16
36
|
attr_reader :raw
|
17
37
|
|
@@ -63,6 +83,11 @@ module Etna
|
|
63
83
|
@raw['auth_redirect']
|
64
84
|
end
|
65
85
|
end
|
86
|
+
|
87
|
+
class JobType < String
|
88
|
+
include Enum
|
89
|
+
REDCAP = JobType.new("redcap")
|
90
|
+
end
|
66
91
|
end
|
67
92
|
end
|
68
93
|
end
|
data/lib/etna/csvs.rb
CHANGED
@@ -27,7 +27,8 @@ module Etna
|
|
27
27
|
lineno += 1
|
28
28
|
row = row.to_hash
|
29
29
|
row.keys.each { |k| row[k].strip! if row[k] =~ /^\s+$/ } if @strip
|
30
|
-
row.
|
30
|
+
row.keys.each { |k| row[k] = "" if row[k].nil? } unless @filter_empties
|
31
|
+
row.select! { |k, v| !v.nil? && !v.empty? } if @filter_empties
|
31
32
|
@row_formatter.call(row) unless @row_formatter.nil?
|
32
33
|
yield row, lineno if block_given?
|
33
34
|
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Etna
|
2
|
+
# A class that encapsulates opening / reading file system entries that abstracts normal file access in order
|
3
|
+
# to make stubbing, substituting, and testing easier.
|
4
|
+
class Filesystem
|
5
|
+
def with_writeable(dest, opts = 'w', &block)
|
6
|
+
::File.open(dest, opts, &block)
|
7
|
+
end
|
8
|
+
|
9
|
+
def with_readable(src, opts = 'r', &block)
|
10
|
+
::File.open(src, opts, &block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def mkdir_p(dir)
|
14
|
+
require 'fileutils'
|
15
|
+
::FileUtils.mkdir_p(dir)
|
16
|
+
end
|
17
|
+
|
18
|
+
def rm_rf(dir)
|
19
|
+
require 'fileutils'
|
20
|
+
FileUtils.rm_rf(dir)
|
21
|
+
end
|
22
|
+
|
23
|
+
def tmpdir
|
24
|
+
::Dir.tmpdir
|
25
|
+
end
|
26
|
+
|
27
|
+
def exist?(src)
|
28
|
+
::File.exist?(src)
|
29
|
+
end
|
30
|
+
|
31
|
+
class EmptyIO < StringIO
|
32
|
+
def write(*args)
|
33
|
+
# Do nothing -- always leave empty
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Mock < Filesystem
|
38
|
+
def initialize(&new_io)
|
39
|
+
@files = {}
|
40
|
+
@dirs = {}
|
41
|
+
@new_io = new_io
|
42
|
+
end
|
43
|
+
|
44
|
+
def mkio(file, opts)
|
45
|
+
if @new_io.nil?
|
46
|
+
StringIO.new
|
47
|
+
else
|
48
|
+
@new_io.call(file, opts)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def with_writeable(dest, opts = 'w', &block)
|
53
|
+
if @dirs.include?(dest)
|
54
|
+
raise IOError.new("Path #{dest} is a directory")
|
55
|
+
end
|
56
|
+
|
57
|
+
dir, file = File.split(dest)
|
58
|
+
@dirs[dir] ||= Set.new
|
59
|
+
@dirs[dir].add(file)
|
60
|
+
yield (@files[dest] = mkio(dest, opts))
|
61
|
+
end
|
62
|
+
|
63
|
+
def mkdir_p(dest)
|
64
|
+
while !@dirs.include?(dest)
|
65
|
+
@dirs[dest] = Set.new
|
66
|
+
break if dest == "." || dest == "/"
|
67
|
+
dest, _ = File.split(dest)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def tmpdir
|
72
|
+
require 'securerandom'
|
73
|
+
"/tmp-#{SecureRandom::uuid}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def with_readable(src, opts = 'r', &block)
|
77
|
+
if @dirs.include?(dest)
|
78
|
+
raise IOError.new("Path #{dest} is a directory")
|
79
|
+
end
|
80
|
+
|
81
|
+
if !@files.include?(dest)
|
82
|
+
raise IOError.new("Path #{dest} does not exist")
|
83
|
+
end
|
84
|
+
|
85
|
+
yield (@files[dest] ||= mkio(src, opts))
|
86
|
+
end
|
87
|
+
|
88
|
+
def exist?(src)
|
89
|
+
@files.include?(src) || @dirs.include?(src)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/etna/spec/vcr.rb
CHANGED
@@ -30,6 +30,8 @@ def setup_base_vcr(spec_helper_dir)
|
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
|
+
# c.debug_logger = File.open('log/vcr_debug.log', 'w')
|
34
|
+
|
33
35
|
c.default_cassette_options = {
|
34
36
|
serialize_with: :compressed,
|
35
37
|
record: if ENV['IS_CI'] == '1'
|
@@ -82,6 +84,11 @@ def setup_base_vcr(spec_helper_dir)
|
|
82
84
|
end
|
83
85
|
end
|
84
86
|
end
|
87
|
+
|
88
|
+
require 'multipartable'
|
89
|
+
def Multipartable.secure_boundary
|
90
|
+
"--THIS-IS-STABLE-FOR-TESTING"
|
91
|
+
end
|
85
92
|
end
|
86
93
|
|
87
94
|
def prepare_vcr_secret
|
data/lib/etna/test_auth.rb
CHANGED
@@ -7,7 +7,7 @@ module Etna
|
|
7
7
|
class TestAuth < Auth
|
8
8
|
def self.token_header(params)
|
9
9
|
token = Base64.strict_encode64(params.to_json)
|
10
|
-
return [ 'Authorization', "Etna
|
10
|
+
return [ 'Authorization', "Etna something.#{token}" ]
|
11
11
|
end
|
12
12
|
|
13
13
|
def self.token_param(params)
|
@@ -36,8 +36,12 @@ module Etna
|
|
36
36
|
|
37
37
|
return false unless token
|
38
38
|
|
39
|
-
#
|
40
|
-
|
39
|
+
# Here we simply base64-encode our user hash and pass it through
|
40
|
+
# In order to behave more like "real" tokens, we expect the user hash to be
|
41
|
+
# in index 1 after splitting by ".".
|
42
|
+
# We do this to support Metis client tests, we pass in tokens with multiple "."-separated parts, so
|
43
|
+
# have to account for that.
|
44
|
+
payload = JSON.parse(Base64.decode64(token.split('.')[1]))
|
41
45
|
|
42
46
|
request.env['etna.user'] = Etna::User.new(payload.map{|k,v| [k.to_sym, v]}.to_h, token)
|
43
47
|
end
|
data/lib/helpers.rb
CHANGED
@@ -6,6 +6,15 @@ module WithEtnaClients
|
|
6
6
|
EtnaApp.instance.environment
|
7
7
|
end
|
8
8
|
|
9
|
+
def exit(status=true)
|
10
|
+
WithEtnaClients.exit(status)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Abstraction used to prevent accidental exist in specs.
|
14
|
+
def self.exit(status)
|
15
|
+
Kernel.exit(status)
|
16
|
+
end
|
17
|
+
|
9
18
|
def token(ignore_environment: false)
|
10
19
|
unless ignore_environment
|
11
20
|
if environment == :many
|
@@ -38,9 +47,6 @@ module WithEtnaClients
|
|
38
47
|
@magma_client ||= Etna::Clients::Magma.new(
|
39
48
|
token: token,
|
40
49
|
ignore_ssl: EtnaApp.instance.config(:ignore_ssl),
|
41
|
-
# Persistent connections cause problem with magma restarts, until we can fix that we should force them
|
42
|
-
# to close + reopen each request.
|
43
|
-
persistent: false,
|
44
50
|
**EtnaApp.instance.config(:magma, environment) || {})
|
45
51
|
end
|
46
52
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: etna
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.25
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Saurabh Asthana
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -98,6 +98,7 @@ files:
|
|
98
98
|
- lib/etna/auth.rb
|
99
99
|
- lib/etna/client.rb
|
100
100
|
- lib/etna/clients.rb
|
101
|
+
- lib/etna/clients/base_client.rb
|
101
102
|
- lib/etna/clients/enum.rb
|
102
103
|
- lib/etna/clients/janus.rb
|
103
104
|
- lib/etna/clients/janus/client.rb
|
@@ -117,9 +118,11 @@ files:
|
|
117
118
|
- lib/etna/clients/magma/workflows/file_linking_workflow.rb
|
118
119
|
- lib/etna/clients/magma/workflows/json_converters.rb
|
119
120
|
- lib/etna/clients/magma/workflows/json_validators.rb
|
121
|
+
- lib/etna/clients/magma/workflows/materialize_magma_record_files_workflow.rb
|
120
122
|
- lib/etna/clients/magma/workflows/model_synchronization_workflow.rb
|
121
123
|
- lib/etna/clients/magma/workflows/record_synchronization_workflow.rb
|
122
124
|
- lib/etna/clients/magma/workflows/update_attributes_from_csv_workflow.rb
|
125
|
+
- lib/etna/clients/magma/workflows/walk_model_tree_workflow.rb
|
123
126
|
- lib/etna/clients/metis.rb
|
124
127
|
- lib/etna/clients/metis/client.rb
|
125
128
|
- lib/etna/clients/metis/models.rb
|
@@ -140,6 +143,7 @@ files:
|
|
140
143
|
- lib/etna/environment_scoped.rb
|
141
144
|
- lib/etna/errors.rb
|
142
145
|
- lib/etna/ext.rb
|
146
|
+
- lib/etna/filesystem.rb
|
143
147
|
- lib/etna/generate_autocompletion_script.rb
|
144
148
|
- lib/etna/hmac.rb
|
145
149
|
- lib/etna/json_serializable_struct.rb
|