etna 0.1.15 → 0.1.21

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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/bin/etna +18 -0
  3. data/etna.completion +1001 -0
  4. data/etna_app.completion +133 -0
  5. data/ext/completions/extconf.rb +20 -0
  6. data/lib/commands.rb +395 -0
  7. data/lib/etna.rb +7 -0
  8. data/lib/etna/application.rb +46 -22
  9. data/lib/etna/client.rb +82 -48
  10. data/lib/etna/clients.rb +4 -0
  11. data/lib/etna/clients/enum.rb +9 -0
  12. data/lib/etna/clients/janus.rb +2 -0
  13. data/lib/etna/clients/janus/client.rb +73 -0
  14. data/lib/etna/clients/janus/models.rb +78 -0
  15. data/lib/etna/clients/magma.rb +4 -0
  16. data/lib/etna/clients/magma/client.rb +80 -0
  17. data/lib/etna/clients/magma/formatting.rb +1 -0
  18. data/lib/etna/clients/magma/formatting/models_csv.rb +354 -0
  19. data/lib/etna/clients/magma/models.rb +630 -0
  20. data/lib/etna/clients/magma/workflows.rb +10 -0
  21. data/lib/etna/clients/magma/workflows/add_project_models_workflow.rb +67 -0
  22. data/lib/etna/clients/magma/workflows/attribute_actions_from_json_workflow.rb +62 -0
  23. data/lib/etna/clients/magma/workflows/create_project_workflow.rb +123 -0
  24. data/lib/etna/clients/magma/workflows/crud_workflow.rb +85 -0
  25. data/lib/etna/clients/magma/workflows/ensure_containing_record_workflow.rb +44 -0
  26. data/lib/etna/clients/magma/workflows/file_attributes_blank_workflow.rb +68 -0
  27. data/lib/etna/clients/magma/workflows/file_linking_workflow.rb +115 -0
  28. data/lib/etna/clients/magma/workflows/json_converters.rb +81 -0
  29. data/lib/etna/clients/magma/workflows/json_validators.rb +452 -0
  30. data/lib/etna/clients/magma/workflows/model_synchronization_workflow.rb +306 -0
  31. data/lib/etna/clients/magma/workflows/record_synchronization_workflow.rb +63 -0
  32. data/lib/etna/clients/magma/workflows/update_attributes_from_csv_workflow.rb +246 -0
  33. data/lib/etna/clients/metis.rb +3 -0
  34. data/lib/etna/clients/metis/client.rb +239 -0
  35. data/lib/etna/clients/metis/models.rb +313 -0
  36. data/lib/etna/clients/metis/workflows.rb +2 -0
  37. data/lib/etna/clients/metis/workflows/metis_download_workflow.rb +37 -0
  38. data/lib/etna/clients/metis/workflows/metis_upload_workflow.rb +137 -0
  39. data/lib/etna/clients/polyphemus.rb +3 -0
  40. data/lib/etna/clients/polyphemus/client.rb +33 -0
  41. data/lib/etna/clients/polyphemus/models.rb +68 -0
  42. data/lib/etna/clients/polyphemus/workflows.rb +1 -0
  43. data/lib/etna/clients/polyphemus/workflows/set_configuration_workflow.rb +47 -0
  44. data/lib/etna/command.rb +243 -5
  45. data/lib/etna/controller.rb +4 -0
  46. data/lib/etna/csvs.rb +159 -0
  47. data/lib/etna/directed_graph.rb +56 -0
  48. data/lib/etna/environment_scoped.rb +19 -0
  49. data/lib/etna/errors.rb +6 -0
  50. data/lib/etna/generate_autocompletion_script.rb +131 -0
  51. data/lib/etna/json_serializable_struct.rb +37 -0
  52. data/lib/etna/logger.rb +24 -2
  53. data/lib/etna/multipart_serializable_nested_hash.rb +50 -0
  54. data/lib/etna/route.rb +1 -1
  55. data/lib/etna/server.rb +3 -0
  56. data/lib/etna/spec/vcr.rb +99 -0
  57. data/lib/etna/templates/attribute_actions_template.json +43 -0
  58. data/lib/etna/test_auth.rb +3 -1
  59. data/lib/etna/user.rb +4 -0
  60. data/lib/helpers.rb +90 -0
  61. metadata +70 -5
@@ -0,0 +1,306 @@
1
+ require 'ostruct'
2
+
3
+ module Etna
4
+ module Clients
5
+ class Magma
6
+ # Note! These workflows are not perfectly atomic, nor perfectly synchronized due to nature of the backend.
7
+ # These primitives are best effort locally synchronized, but cannot defend the backend or simultaneous
8
+ # system updates.
9
+ class ModelSynchronizationWorkflow < Struct.new(
10
+ :target_client, :source_models, :target_project, :update_block,
11
+ :model_name, :plan_only, :validate, :use_versions, :renames,
12
+ keyword_init: true)
13
+ def target_models
14
+ @target_models ||= begin
15
+ target_client.retrieve(RetrievalRequest.new(project_name: self.target_project, model_name: 'all')).models
16
+ end
17
+ end
18
+
19
+ def planned_actions
20
+ @planned_actions ||= []
21
+ end
22
+
23
+ def queue_update(action)
24
+ if plan_only
25
+ plan_update(action)
26
+ else
27
+ execute_update(action)
28
+ end
29
+ end
30
+
31
+ def execute_planned!
32
+ @planned_actions.each do |action|
33
+ execute_update(action)
34
+ end
35
+ end
36
+
37
+ def execute_update(action)
38
+ update = UpdateModelRequest.new(project_name: self.target_project)
39
+ update_block.call(action) if update_block
40
+ update.add_action(action)
41
+ @target_models = nil
42
+ target_client.update_model(update)
43
+ end
44
+
45
+ def copy_link_into_target(link, reciprocal)
46
+ attr = target_models.build_model(link.model_name).build_template.build_attributes.build_attribute(link.attribute_name)
47
+ attr.attribute_type = link.type
48
+ attr.name = attr.attribute_name = link.attribute_name
49
+ attr.link_model_name = reciprocal.model_name
50
+ end
51
+
52
+ # Applies the given action to the source models and 'plans' its execution.
53
+ def plan_update(action)
54
+ case action
55
+ when UpdateAttributeAction
56
+ attribute_update = target_models.build_model(action.model_name).build_template.build_attributes.build_attribute(action.attribute_name)
57
+ Attribute.copy(action, attribute_update, attributes: Attribute::EDITABLE_ATTRIBUTE_ATTRIBUTES)
58
+ when AddAttributeAction
59
+ new_attribute = target_models.build_model(action.model_name).build_template.build_attributes.build_attribute(action.attribute_name)
60
+ Attribute.copy(action, new_attribute)
61
+ when AddLinkAction
62
+ first_link = action.links[0]
63
+ second_link = action.links[1]
64
+ copy_link_into_target(first_link, second_link)
65
+ copy_link_into_target(second_link, first_link)
66
+ when AddModelAction
67
+ template = target_models.build_model(action.model_name).build_template
68
+ template.name = action.model_name
69
+ template.identifier = action.identifier
70
+ template.parent = action.parent_model_name
71
+
72
+ child_link = AddLinkDefinition.new(type: AttributeType::PARENT, model_name: template.name, attribute_name: template.parent)
73
+ parent_link = AddLinkDefinition.new(type: action.parent_link_type, model_name: template.parent, attribute_name: template.name)
74
+
75
+ copy_link_into_target(child_link, parent_link)
76
+ copy_link_into_target(parent_link, child_link)
77
+
78
+ ['created_at', 'updated_at'].each do |time_attr_name|
79
+ template.build_attributes.build_attribute(time_attr_name).tap do |attr|
80
+ attr.attribute_type = Etna::Clients::Magma::AttributeType::DATE_TIME
81
+ attr.attribute_name = time_attr_name
82
+ end
83
+ end
84
+
85
+ if action.parent_link_type != Etna::Clients::Magma::AttributeType::TABLE
86
+ template.build_attributes.build_attribute(template.identifier).tap do |attr|
87
+ attr.attribute_name = template.identifier
88
+ attr.attribute_type = Etna::Clients::Magma::AttributeType::IDENTIFIER
89
+ end
90
+ end
91
+ when RenameAttributeAction
92
+ attributes = target_models.model(action.model_name).template.attributes
93
+ attributes.raw[action.new_attribute_name] = attributes.raw.delete(action.attribute_name)
94
+ else
95
+ raise "Unexpected plan_action #{action}"
96
+ end
97
+
98
+ planned_actions << action
99
+ end
100
+
101
+
102
+ def self.from_api_source(source_project:, source_client:, **kwds)
103
+ self.new(
104
+ source_models: source_client.retrieve(RetrievalRequest.new(project_name: source_project, model_name: 'all')).models,
105
+ **kwds
106
+ )
107
+ end
108
+
109
+ # Subclass and override when the source <-> target mapping is not 1:1.
110
+ def target_of_source(model_name)
111
+ model_name
112
+ end
113
+
114
+ # Subclass and override when the source <-> target attribute mapping is not 1:1.
115
+ def target_attribute_of_source(model_name, attribute_name)
116
+ attribute_name
117
+ end
118
+
119
+ # Potentially cyclical, protected against re-entry via the seen cache.
120
+ # Establishes the link attributes in a given model graph.
121
+ def ensure_model_tree(model_name, seen = Set.new)
122
+ return unless (source_model = source_models.model(model_name))
123
+ return if seen.include?(model_name)
124
+ seen.add(model_name)
125
+ ensure_model(model_name)
126
+
127
+ attributes = source_model.template.attributes
128
+
129
+ attributes.all.each do |attribute|
130
+ # Don't copy or update parent links. Once a model has been setup with a parent someway.
131
+ unless attribute.attribute_type == AttributeType::PARENT
132
+ if attribute.link_model_name
133
+ ensure_model(attribute.link_model_name)
134
+ ensure_model_link(model_name, attribute.link_model_name, attribute.attribute_name)
135
+ else
136
+ ensure_model_attribute(model_name, attribute.attribute_name)
137
+ end
138
+ end
139
+
140
+ # Even if it's a parent node, however, we still want to cascade the tree expansion to all links.
141
+ unless attribute.link_model_name.nil?
142
+ ensure_model_tree(attribute.link_model_name, seen)
143
+ end
144
+ end
145
+ end
146
+
147
+ def ensure_model_link(model_name, link_model_name, attribute_name)
148
+ return unless (model = source_models.model(model_name))
149
+ return unless (source_attribute = model.template.attributes.attribute(attribute_name))
150
+
151
+ return unless (link_model = source_models.model(link_model_name))
152
+ link_model_attributes = link_model.template.attributes
153
+ reciprocal = link_model_attributes.all.find do |attr|
154
+ attr.link_model_name == model_name
155
+ end
156
+
157
+ target_model_name = target_of_source(model_name)
158
+ target_link_model_name = target_of_source(link_model_name)
159
+
160
+ target_attributes = target_models.model(target_model_name).template.attributes
161
+ return if target_attributes.attribute_keys.include?(target_link_model_name)
162
+
163
+ add_link = AddLinkAction.new
164
+ add_link.links << AddLinkDefinition.new(model_name: target_model_name, attribute_name: target_link_model_name, type: source_attribute.attribute_type)
165
+ add_link.links << AddLinkDefinition.new(model_name: target_link_model_name, attribute_name: reciprocal.attribute_name, type: reciprocal.attribute_type)
166
+
167
+ queue_update(add_link)
168
+ end
169
+
170
+ def ensure_model_attribute(model_name, attribute_name)
171
+ return unless (model = source_models.model(model_name))
172
+ return unless (source_attribute = model.template.attributes.attribute(attribute_name))
173
+
174
+ target_model_name = target_of_source(model_name)
175
+ target_attribute, target_attribute_name = ensure_model_attribute_target_rename(model_name, attribute_name)
176
+
177
+ if target_attribute.nil?
178
+ add_attribute = AddAttributeAction.new(
179
+ model_name: target_model_name,
180
+ attribute_name: target_attribute_name,
181
+ )
182
+
183
+ Attribute.copy(source_attribute, add_attribute)
184
+ queue_update(add_attribute)
185
+ else
186
+ # If there are is no diff, don't produce an action.
187
+ target_attribute = Attribute.new(target_attribute.raw)
188
+ target_attribute.set_field_defaults!
189
+
190
+ source_attribute = Attribute.new(source_attribute.raw)
191
+ source_attribute.set_field_defaults!
192
+
193
+ source_editable = source_attribute.raw.slice(*Attribute::EDITABLE_ATTRIBUTE_ATTRIBUTES.map(&:to_s))
194
+ target_editable = target_attribute.raw.slice(*Attribute::EDITABLE_ATTRIBUTE_ATTRIBUTES.map(&:to_s))
195
+
196
+ if source_editable == target_editable
197
+ return
198
+ end
199
+
200
+ update_attribute = UpdateAttributeAction.new(
201
+ model_name: target_model_name,
202
+ attribute_name: target_attribute_name,
203
+ )
204
+
205
+ Attribute.copy(source_attribute, update_attribute, attributes: Attribute::EDITABLE_ATTRIBUTE_ATTRIBUTES)
206
+ queue_update(update_attribute)
207
+ end
208
+ end
209
+
210
+ # Returns a tuple of the target's attribute, post rename if necessary, if it exists, and the name of the target attribute
211
+ # cases here:
212
+ # 1. There is no rename for the attribute
213
+ # a. There target attribute already exists -> [target_attribute, attribute_name]
214
+ # b. The target attribute does not exist -> [nil, attribute_name]
215
+ # 2. There is an expected rename from the source
216
+ # a. The target has neither the renamed attribute or the original attribute -> [nil, new_attribute_name]
217
+ # b. The target has the renamed attribute already -> [renamed_attribute, new_attribute_name]
218
+ # c. The target has the source attribute, which is not yet renamed. -> [renamed_attribute, new_attribute_name]
219
+ def ensure_model_attribute_target_rename(model_name, attribute_name)
220
+ target_model_name = target_of_source(model_name)
221
+ target_attribute_name = target_attribute_of_source(model_name, attribute_name)
222
+ return nil unless (target_model = target_models.model(target_model_name))
223
+
224
+ target_original_attribute = target_model.template.attributes.attribute(target_attribute_name)
225
+
226
+ if renames && (attribute_renames = renames[model_name]) && (new_name = attribute_renames[attribute_name])
227
+ new_name = target_attribute_of_source(model_name, new_name)
228
+
229
+ unless target_model.template.attributes.include?(new_name)
230
+ if target_original_attribute
231
+ rename = RenameAttributeAction.new(model_name: target_model_name, attribute_name: target_attribute_name, new_attribute_name: new_name)
232
+ queue_update(rename)
233
+ else
234
+ return [nil, new_name]
235
+ end
236
+ end
237
+
238
+ return [target_model.template.attributes.attribute(new_name), new_name]
239
+ end
240
+
241
+ [target_original_attribute, target_attribute_name]
242
+ end
243
+
244
+ # Non cyclical, non re-entrant due to the requirement that parents cannot form a cycle.
245
+ # This method, and it's partner prepare_parent, should never call into any re-entrant or potentially
246
+ # cyclical method, like ensure_model_tree.
247
+ def ensure_model(model_name)
248
+ return unless (source_model = source_models.model(model_name))
249
+ target_model_name = target_of_source(model_name)
250
+ return if target_models.model_keys.include?(target_model_name)
251
+
252
+ template = source_model.template
253
+
254
+ add_model_action = AddModelAction.new(
255
+ {
256
+ model_name: target_model_name,
257
+ identifier: template.identifier,
258
+ }
259
+ )
260
+
261
+ parents = template.attributes.all.select { |a| a.attribute_type == AttributeType::PARENT }
262
+ parent_model_name, parent_link_type = prepare_parent(model_name, template, parents)
263
+ unless parent_model_name.nil?
264
+ add_model_action.parent_model_name = parent_model_name
265
+ add_model_action.parent_link_type = parent_link_type
266
+ end
267
+
268
+ queue_update(add_model_action)
269
+ end
270
+
271
+ # Non cyclical, non re-entrant due to the requirement that parents cannot form a cycle.
272
+ # This method, and it's partner and ensure_model, should never call into any re-entrant or potentially
273
+ # cyclical method, like ensure_model_tree.
274
+ def prepare_parent(model_name, template, parents)
275
+ return [nil, nil] if parents.empty?
276
+ return [nil, nil] unless (parent_model = source_models.model(template.parent))
277
+ return [nil, nil] unless (child_attribute = parent_model.template.attributes.attribute(model_name))
278
+
279
+ ensure_model(template.parent)
280
+
281
+ [
282
+ target_of_source(template.parent),
283
+ child_attribute.attribute_type
284
+ ]
285
+ end
286
+
287
+ def self.models_affected_by(action)
288
+ case action
289
+ when Etna::Clients::Magma::RenameAttributeAction
290
+ [action.model_name]
291
+ when Etna::Clients::Magma::UpdateAttributeAction
292
+ [action.model_name]
293
+ when Etna::Clients::Magma::AddAttributeAction
294
+ [action.model_name]
295
+ when Etna::Clients::Magma::AddModelAction
296
+ [action.model_name]
297
+ when Etna::Clients::Magma::AddLinkAction
298
+ action.links.map(&:model_name)
299
+ else
300
+ []
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,63 @@
1
+ require 'ostruct'
2
+
3
+ module Etna
4
+ module Clients
5
+ class Magma
6
+ class RecordSynchronizationWorkflow < Struct.new(:target_client, :source_client, :project_name, :ignore_update_errors, keyword_init: true)
7
+ def target_models
8
+ @target_models ||= begin
9
+ target_client.retrieve(RetrievalRequest.new(project_name: self.project_name, model_name: 'all')).models
10
+ end
11
+ end
12
+
13
+ def crud
14
+ @crud ||= MagmaCrudWorkflow.new(project_name: project_name, magma_client: target_client)
15
+ end
16
+
17
+ def source_models
18
+ @source_models ||= source_client.retrieve(
19
+ RetrievalRequest.new(
20
+ project_name: project_name, model_name: 'all')).models
21
+ end
22
+
23
+ # TODO: Add paging here to support large payloads
24
+ def copy_model(model_name, copied_models = Set.new)
25
+ if copied_models.include?(model_name)
26
+ return
27
+ end
28
+
29
+ copied_models.add(model_name)
30
+
31
+ descendants = source_models.to_directed_graph.descendants("project")
32
+ (descendants[model_name] || []).each do |required_model|
33
+ copy_model(required_model, copied_models) do |update|
34
+ yield update if block_given?
35
+ end
36
+ end
37
+
38
+ crud.page_records(model_name) do |documents|
39
+ yield [model_name, documents] if block_given?
40
+ begin
41
+ crud.update_records do |update_request|
42
+ documents.document_keys.each do |identifier|
43
+ record = documents.document(identifier)
44
+ record = record.each.select { |k, v| v != nil && !v.is_a?(Array) }.to_h
45
+ update_request.update_revision(model_name, identifier, record)
46
+ end
47
+ end
48
+ rescue => e
49
+ raise unless ignore_update_errors
50
+ end
51
+ end
52
+ end
53
+
54
+ def copy_all_models
55
+ copied_models = Set.new
56
+ target_models.model_keys.each do |model_name|
57
+ copy_model(model_name, copied_models)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,246 @@
1
+ require 'csv'
2
+ require 'ostruct'
3
+ require_relative './crud_workflow'
4
+
5
+ module Etna
6
+ module Clients
7
+ class Magma
8
+ class UpdateAttributesFromCsvWorkflowBase < Struct.new(:magma_crud, :project_name, :filepath, :model_name, :json_values, :hole_value, keyword_init: true)
9
+ def initialize(opts)
10
+ super(**{}.update(opts))
11
+ end
12
+
13
+ def magma_client
14
+ magma_crud.magma_client
15
+ end
16
+
17
+ def parse_input_file
18
+ raise "Must be implemented in a subclass"
19
+ end
20
+
21
+ def model_exists?(model_name)
22
+ models.model(model_name)
23
+ end
24
+
25
+ def consolidate_attributes_to_hash(row)
26
+ raise "Must be implemented in a subclass"
27
+ end
28
+
29
+ def find_attribute(model_name, attribute_name)
30
+ models.model(model_name).template.attributes.attribute(attribute_name)
31
+ end
32
+
33
+ def each_revision
34
+ parse_input_file do |model_name, record_name, revision|
35
+ yield [model_name, record_name, revision]
36
+ end
37
+ end
38
+
39
+ def update_attributes
40
+ method = json_values ? :update_json : :update
41
+ magma_crud.update_records(method: method) do |update_request|
42
+ each_revision do |model_name, record_name, revision|
43
+ update_request.update_revision(model_name, record_name, revision)
44
+ end
45
+ end
46
+ end
47
+
48
+ def models
49
+ @models ||= begin
50
+ magma_client.retrieve(RetrievalRequest.new(project_name: self.project_name, model_name: 'all')).models
51
+ end
52
+ end
53
+ end
54
+
55
+ class RowBase
56
+ def stripped_value(attribute_value)
57
+ attribute_value = attribute_value&.strip
58
+
59
+ if attribute_value && @workflow.json_values && attribute_value != @workflow.hole_value
60
+ attribute_value = JSON.parse(attribute_value)
61
+ end
62
+ attribute_value
63
+ end
64
+
65
+ def nil_or_empty?(value)
66
+ value.nil? || value.empty?
67
+ end
68
+ end
69
+
70
+ class UpdateAttributesFromCsvWorkflowMultiModel < UpdateAttributesFromCsvWorkflowBase
71
+ def parse_input_file
72
+ CSV.parse(File.read(filepath)).map do |csv_row|
73
+ row = Row.new(csv_row, self)
74
+
75
+ raise "Invalid model \"#{row.model_name}\" for project #{project_name}." unless model_exists?(row.model_name)
76
+
77
+ yield [row.model_name, row.record_name, row.to_h]
78
+ end
79
+ end
80
+
81
+ class Row < RowBase
82
+ attr_reader :model_name, :record_name
83
+
84
+ def initialize(raw, workflow)
85
+ # Assumes rows are in pairs, where
86
+ # [0] = model_name
87
+ # [1] = record_name / identifier
88
+ # [2], [4], etc. = attribute_name
89
+ # [3], [5], etc. = attribute value
90
+ # So not every row needs the same number of columns
91
+ @raw = raw
92
+ @workflow = workflow
93
+
94
+ raise "Invalid revision row #{@raw}. Must include at least 4 column values (model,record_name,attribute_name,attribute_value)." if @raw.length < 4
95
+ raise "Invalid revision row #{@raw}. Must have an even number of columns." if @raw.length.odd?
96
+
97
+ @model_name = raw[0]
98
+
99
+ raise "Invalid model name: \"#{@model_name}\"." if nil_or_empty?(@model_name)
100
+
101
+ @model_name.strip!
102
+
103
+ @record_name = raw[1]
104
+
105
+ raise "Invalid record name: \"#{@record_name}\"." if nil_or_empty?(@record_name)
106
+
107
+ @record_name.strip!
108
+ end
109
+
110
+ def to_h
111
+ # Take attribute index values (even) and put them into a hash.
112
+ # The assigned value will be the subsequent odd index.
113
+ # {attribute_name: attribute_value}
114
+ {}.tap do |attributes|
115
+ (2..(@raw.length - 1)).to_a.each do |index|
116
+ if index.even?
117
+ attribute_name = @raw[index]
118
+
119
+ raise "Invalid attribute name: \"#{attribute_name}\"." if nil_or_empty?(attribute_name)
120
+ attribute_name.strip!
121
+
122
+ unless (attribute = @workflow.find_attribute(model_name, attribute_name))
123
+ raise "Invalid attribute #{attribute_name} for model #{model_name}."
124
+ end
125
+
126
+ stripped = stripped_value(@raw[index + 1])
127
+ unless @workflow.hole_value.nil?
128
+ next if stripped == @workflow.hole_value
129
+ end
130
+
131
+ if attribute.is_project_name_reference?(model_name)
132
+ stripped&.downcase!
133
+ end
134
+
135
+ attributes[attribute_name] = stripped
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ class SimpleFileLinkingWorkflow < Struct.new(:metis_client, :project_name, :bucket_name, :folder, :extension, :attribute_name, :regex, :file_collection, keyword_init: true)
144
+ def write_csv_io(filename: nil, output_io: nil)
145
+ exporter = Etna::CsvExporter.new([:identifier, attribute_name.to_sym])
146
+ exporter.with_row_writeable(filename: filename, output_io: output_io) do |row_writeable|
147
+ find_matching_revisions.each do |identifier, value|
148
+ row_writeable << { identifier: identifier, attribute_name.to_sym => value.to_json }
149
+ end
150
+ end
151
+ end
152
+
153
+ def find_matching_revisions
154
+ {}.tap do |revisions|
155
+ metis_client.find(
156
+ Etna::Clients::Metis::FindRequest.new(
157
+ project_name: project_name,
158
+ bucket_name: bucket_name,
159
+ params: [Etna::Clients::Metis::FindParam.new(
160
+ attribute: 'name',
161
+ predicate: 'glob',
162
+ value: "#{folder}/**/*.#{extension}",
163
+ type: 'file'
164
+ )]
165
+ )).files.all.each do |file|
166
+ puts "Checking #{file.file_path}"
167
+ match = regex.match(file.file_path)
168
+ if match
169
+ match_map = match.names.zip(match.captures).to_h
170
+ if !match_map.include?('identifier')
171
+ raise "Regex #{regex.source} does not include a ?<identifier> named matcher, please add one to regulate how identifiers are created."
172
+ end
173
+
174
+ puts "Found match"
175
+
176
+ revision = { 'path' => "metis://#{project_name}/#{bucket_name}/#{file.file_path}", 'original_filename' => "#{File.basename(file.file_path)}" }
177
+ if file_collection
178
+ collection = revisions[match_map['identifier']] ||= []
179
+ collection << revision
180
+ else
181
+ record = revisions[match_map['identifier']] ||= {}
182
+ unless record.empty?
183
+ raise "Multiple files match #{match_map['identifier']}, found #{record['path']} and #{revision['path']}"
184
+ end
185
+ record.update(revision)
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ class UpdateAttributesFromCsvWorkflowSingleModel < UpdateAttributesFromCsvWorkflowBase
194
+ def initialize(opts)
195
+ super(**{}.update(opts))
196
+ raise "Single Model invocation must include keyword :model_name." if !opts[:model_name]
197
+ raise "Invalid model #{model_name} for project #{project_name}." unless model_exists?(model_name)
198
+ end
199
+
200
+ def parse_input_file
201
+ CSV.parse(File.read(filepath), headers: true).map do |csv_row|
202
+ row = Row.new(csv_row, model_name, self)
203
+
204
+ yield [model_name, row.record_name, row.to_h]
205
+ end
206
+ end
207
+
208
+ class Row < RowBase
209
+ attr_reader :record_name
210
+
211
+ def initialize(raw, model_name, workflow)
212
+ # Assumes CSV includes a column header to identify the attribute_name
213
+ # Assumes index 0 is the record_name
214
+ @raw = raw
215
+ @model_name = model_name
216
+ @workflow = workflow
217
+
218
+ @record_name = @raw[0]
219
+ raise "Invalid record name: \"#{record_name}\"." if nil_or_empty?(record_name)
220
+
221
+ @record_name.strip!
222
+ end
223
+
224
+ def to_h
225
+ # Row can be converted to a hash, where keys are attribute_names and the
226
+ # values come from the CSV
227
+ # {attribute_name: attribute_value}
228
+ {}.tap do |attributes|
229
+ row_hash = @raw.to_h
230
+ row_keys = row_hash.keys
231
+ row_keys[1..row_keys.length - 1].each do |attribute_name|
232
+ raise "Invalid attribute name: \"#{attribute_name}\"." if nil_or_empty?(attribute_name)
233
+
234
+ attribute_name_clean = attribute_name.strip
235
+ raise "Invalid attribute \"#{attribute_name_clean}\" for model #{@model_name}." unless attribute = @workflow.find_attribute(@model_name, attribute_name_clean)
236
+
237
+ attributes[attribute_name_clean] = stripped_value(@raw[attribute_name])
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
246
+