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.
@@ -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
- attr_reader :token
13
- def initialize(host:, token:, persistent: true, ignore_ssl: false)
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
@@ -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.select! { |k, v| !v.empty? } if @filter_empties
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
@@ -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
@@ -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 #{token}" ]
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
- # here we simply base64-encode our user hash and pass it through
40
- payload = JSON.parse(Base64.decode64(token))
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
@@ -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.20
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: 2020-12-14 00:00:00.000000000 Z
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