etna 0.1.12 → 0.1.18
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/bin/etna +63 -0
- data/etna.completion +926 -0
- data/etna_app.completion +133 -0
- data/ext/completions/extconf.rb +20 -0
- data/lib/commands.rb +368 -0
- data/lib/etna.rb +6 -0
- data/lib/etna/application.rb +46 -22
- data/lib/etna/client.rb +82 -48
- data/lib/etna/clients.rb +4 -0
- data/lib/etna/clients/enum.rb +9 -0
- data/lib/etna/clients/janus.rb +2 -0
- data/lib/etna/clients/janus/client.rb +73 -0
- data/lib/etna/clients/janus/models.rb +78 -0
- data/lib/etna/clients/magma.rb +4 -0
- data/lib/etna/clients/magma/client.rb +80 -0
- data/lib/etna/clients/magma/formatting.rb +1 -0
- data/lib/etna/clients/magma/formatting/models_csv.rb +345 -0
- data/lib/etna/clients/magma/models.rb +579 -0
- data/lib/etna/clients/magma/workflows.rb +10 -0
- data/lib/etna/clients/magma/workflows/add_project_models_workflow.rb +78 -0
- data/lib/etna/clients/magma/workflows/attribute_actions_from_json_workflow.rb +62 -0
- data/lib/etna/clients/magma/workflows/create_project_workflow.rb +117 -0
- data/lib/etna/clients/magma/workflows/crud_workflow.rb +85 -0
- data/lib/etna/clients/magma/workflows/ensure_containing_record_workflow.rb +44 -0
- data/lib/etna/clients/magma/workflows/file_attributes_blank_workflow.rb +68 -0
- data/lib/etna/clients/magma/workflows/file_linking_workflow.rb +115 -0
- data/lib/etna/clients/magma/workflows/json_converters.rb +81 -0
- data/lib/etna/clients/magma/workflows/json_validators.rb +447 -0
- data/lib/etna/clients/magma/workflows/model_synchronization_workflow.rb +306 -0
- data/lib/etna/clients/magma/workflows/record_synchronization_workflow.rb +63 -0
- data/lib/etna/clients/magma/workflows/update_attributes_from_csv_workflow.rb +178 -0
- data/lib/etna/clients/metis.rb +3 -0
- data/lib/etna/clients/metis/client.rb +239 -0
- data/lib/etna/clients/metis/models.rb +313 -0
- data/lib/etna/clients/metis/workflows.rb +2 -0
- data/lib/etna/clients/metis/workflows/metis_download_workflow.rb +37 -0
- data/lib/etna/clients/metis/workflows/metis_upload_workflow.rb +137 -0
- data/lib/etna/clients/polyphemus.rb +3 -0
- data/lib/etna/clients/polyphemus/client.rb +33 -0
- data/lib/etna/clients/polyphemus/models.rb +68 -0
- data/lib/etna/clients/polyphemus/workflows.rb +1 -0
- data/lib/etna/clients/polyphemus/workflows/set_configuration_workflow.rb +47 -0
- data/lib/etna/command.rb +243 -5
- data/lib/etna/controller.rb +4 -0
- data/lib/etna/directed_graph.rb +56 -0
- data/lib/etna/environment_scoped.rb +19 -0
- data/lib/etna/generate_autocompletion_script.rb +130 -0
- data/lib/etna/hmac.rb +1 -0
- data/lib/etna/json_serializable_struct.rb +37 -0
- data/lib/etna/logger.rb +15 -1
- data/lib/etna/multipart_serializable_nested_hash.rb +50 -0
- data/lib/etna/parse_body.rb +1 -1
- data/lib/etna/route.rb +1 -1
- data/lib/etna/server.rb +3 -0
- data/lib/etna/spec/vcr.rb +98 -0
- data/lib/etna/templates/attribute_actions_template.json +43 -0
- data/lib/etna/test_auth.rb +4 -2
- data/lib/etna/user.rb +11 -1
- data/lib/helpers.rb +81 -0
- metadata +70 -7
@@ -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::STRING
|
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,178 @@
|
|
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, 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
|
+
magma_crud.update_records do |update_request|
|
41
|
+
each_revision do |model_name, record_name, revision|
|
42
|
+
update_request.update_revision(model_name, record_name, revision)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def models
|
48
|
+
@models ||= begin
|
49
|
+
magma_client.retrieve(RetrievalRequest.new(project_name: self.project_name, model_name: 'all')).models
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class RowBase
|
55
|
+
def stripped_value(attribute_value)
|
56
|
+
attribute_value ? attribute_value.strip : attribute_value
|
57
|
+
end
|
58
|
+
|
59
|
+
def nil_or_empty?(value)
|
60
|
+
value.nil? || value.empty?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class UpdateAttributesFromCsvWorkflowMultiModel < UpdateAttributesFromCsvWorkflowBase
|
65
|
+
def parse_input_file
|
66
|
+
CSV.parse(File.read(filepath)).map do |csv_row|
|
67
|
+
row = Row.new(csv_row, self)
|
68
|
+
|
69
|
+
raise "Invalid model \"#{row.model_name}\" for project #{project_name}." unless model_exists?(row.model_name)
|
70
|
+
|
71
|
+
yield [row.model_name, row.record_name, row.to_h]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Row < RowBase
|
76
|
+
attr_reader :model_name, :record_name
|
77
|
+
def initialize(raw, workflow)
|
78
|
+
# Assumes rows are in pairs, where
|
79
|
+
# [0] = model_name
|
80
|
+
# [1] = record_name / identifier
|
81
|
+
# [2], [4], etc. = attribute_name
|
82
|
+
# [3], [5], etc. = attribute value
|
83
|
+
# So not every row needs the same number of columns
|
84
|
+
@raw = raw
|
85
|
+
@workflow = workflow
|
86
|
+
|
87
|
+
raise "Invalid revision row #{@raw}. Must include at least 4 column values (model,record_name,attribute_name,attribute_value)." if @raw.length < 4
|
88
|
+
raise "Invalid revision row #{@raw}. Must have an even number of columns." if @raw.length.odd?
|
89
|
+
|
90
|
+
@model_name = raw[0]
|
91
|
+
|
92
|
+
raise "Invalid model name: \"#{@model_name}\"." if nil_or_empty?(@model_name)
|
93
|
+
|
94
|
+
@model_name.strip!
|
95
|
+
|
96
|
+
@record_name = raw[1]
|
97
|
+
|
98
|
+
raise "Invalid record name: \"#{@record_name}\"." if nil_or_empty?(@record_name)
|
99
|
+
|
100
|
+
@record_name.strip!
|
101
|
+
end
|
102
|
+
|
103
|
+
def to_h
|
104
|
+
# Take attribute index values (even) and put them into a hash.
|
105
|
+
# The assigned value will be the subsequent odd index.
|
106
|
+
# {attribute_name: attribute_value}
|
107
|
+
{}.tap do |attributes|
|
108
|
+
(2..(@raw.length - 1)).to_a.each do |index|
|
109
|
+
if index.even?
|
110
|
+
attribute_name = @raw[index]
|
111
|
+
|
112
|
+
raise "Invalid attribute name: \"#{attribute_name}\"." if nil_or_empty?(attribute_name)
|
113
|
+
attribute_name.strip!
|
114
|
+
|
115
|
+
raise "Invalid attribute #{attribute_name} for model #{model_name}." unless attribute = @workflow.find_attribute(model_name, attribute_name)
|
116
|
+
|
117
|
+
attributes[attribute_name] = stripped_value(@raw[index + 1])
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class UpdateAttributesFromCsvWorkflowSingleModel < UpdateAttributesFromCsvWorkflowBase
|
126
|
+
def initialize(opts)
|
127
|
+
super(**{}.update(opts))
|
128
|
+
raise "Single Model invokation must include keyword :model_name." if !opts[:model_name]
|
129
|
+
raise "Invalid model #{model_name} for project #{project_name}." unless model_exists?(model_name)
|
130
|
+
end
|
131
|
+
|
132
|
+
def parse_input_file
|
133
|
+
CSV.parse(File.read(filepath), headers: true).map do |csv_row|
|
134
|
+
row = Row.new(csv_row, model_name, self)
|
135
|
+
|
136
|
+
yield [model_name, row.record_name, row.to_h]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class Row < RowBase
|
141
|
+
attr_reader :record_name
|
142
|
+
def initialize(raw, model_name, workflow)
|
143
|
+
# Assumes CSV includes a column header to identify the attribute_name
|
144
|
+
# Assumes index 0 is the record_name
|
145
|
+
@raw = raw
|
146
|
+
@model_name = model_name
|
147
|
+
@workflow = workflow
|
148
|
+
|
149
|
+
@record_name = @raw[0]
|
150
|
+
raise "Invalid record name: \"#{record_name}\"." if nil_or_empty?(record_name)
|
151
|
+
|
152
|
+
@record_name.strip!
|
153
|
+
end
|
154
|
+
|
155
|
+
def to_h
|
156
|
+
# Row can be converted to a hash, where keys are attribute_names and the
|
157
|
+
# values come from the CSV
|
158
|
+
# {attribute_name: attribute_value}
|
159
|
+
{}.tap do |attributes|
|
160
|
+
row_hash = @raw.to_h
|
161
|
+
row_keys = row_hash.keys
|
162
|
+
row_keys[1..row_keys.length - 1].each do |attribute_name|
|
163
|
+
|
164
|
+
raise "Invalid attribute name: \"#{attribute_name}\"." if nil_or_empty?(attribute_name)
|
165
|
+
|
166
|
+
attribute_name_clean = attribute_name.strip
|
167
|
+
raise "Invalid attribute \"#{attribute_name_clean}\" for model #{@model_name}." unless attribute = @workflow.find_attribute(@model_name, attribute_name_clean)
|
168
|
+
|
169
|
+
attributes[attribute_name_clean] = stripped_value(@raw[attribute_name])
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|