eco-helpers 1.0.13 → 1.0.14

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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/lib/eco/api/common/people/entry_factory.rb +3 -1
  3. data/lib/eco/api/common/people/person_attribute_parser.rb +33 -10
  4. data/lib/eco/api/common/people/person_entry.rb +1 -1
  5. data/lib/eco/api/common/people/person_entry_attribute_mapper.rb +1 -1
  6. data/lib/eco/api/common/people/person_factory.rb +5 -1
  7. data/lib/eco/api/common/session/environment.rb +7 -3
  8. data/lib/eco/api/common/session/mailer.rb +4 -0
  9. data/lib/eco/api/common/session/sftp.rb +4 -3
  10. data/lib/eco/api/error.rb +1 -0
  11. data/lib/eco/api/organization/presets_factory.rb +1 -1
  12. data/lib/eco/api/organization/tag_tree.rb +1 -1
  13. data/lib/eco/api/session.rb +119 -74
  14. data/lib/eco/api/session/batch.rb +23 -25
  15. data/lib/eco/api/session/batch/base_policy.rb +283 -0
  16. data/lib/eco/api/session/batch/errors.rb +17 -3
  17. data/lib/eco/api/session/batch/feedback.rb +112 -0
  18. data/lib/eco/api/session/batch/job.rb +90 -87
  19. data/lib/eco/api/session/batch/policies.rb +22 -0
  20. data/lib/eco/api/session/batch/request_stats.rb +195 -0
  21. data/lib/eco/api/session/batch/status.rb +66 -19
  22. data/lib/eco/api/session/config.rb +10 -0
  23. data/lib/eco/api/session/config/workflow.rb +1 -1
  24. data/lib/eco/api/usecases/default_cases/set_default_tag_case.rb +4 -3
  25. data/lib/eco/api/usecases/default_cases/switch_supervisor_case.rb +15 -10
  26. data/lib/eco/cli/config/default/filters.rb +3 -2
  27. data/lib/eco/cli/config/default/options.rb +12 -0
  28. data/lib/eco/cli/config/default/usecases.rb +6 -4
  29. data/lib/eco/cli/config/default/workflow.rb +3 -2
  30. data/lib/eco/cli/scripting/args_helpers.rb +1 -1
  31. data/lib/eco/version.rb +1 -1
  32. metadata +5 -1
@@ -45,7 +45,7 @@ module Eco
45
45
  params = {per_page: DEFAULT_BATCH_BLOCK}.merge(params)
46
46
 
47
47
  launch(data, method: :get, params: params, silent: silent).tap do |status|
48
- status.type = :search
48
+ status.mode = :search
49
49
 
50
50
  entries = status.queue
51
51
  puts "\n"
@@ -86,10 +86,6 @@ module Eco
86
86
 
87
87
  private
88
88
 
89
- def new_status(queue, method)
90
- Batch::Status.new(enviro, queue: queue, method: method)
91
- end
92
-
93
89
  def get(params: {}, silent: false)
94
90
  fatal "cannot batch get without api connnection, please provide a valid api connection!" unless people_api = api&.people
95
91
 
@@ -97,7 +93,7 @@ module Eco
97
93
  client = people_api.client
98
94
 
99
95
  looping = !params.key?(:page)
100
- page = params[:page] || 1
96
+ page = params[:page] || 1
101
97
 
102
98
  people = []; total_pages = nil
103
99
  cursor_id = nil
@@ -129,7 +125,7 @@ module Eco
129
125
  def client_get(client, params:, silent: false)
130
126
  response = client.get("/people", params: params)
131
127
  unless response.success?
132
- msg = "Request failed - params: #{params}"
128
+ msg = "Request failed - params: #{params}"
133
129
  msg += "\n Error message: - Status #{response.status}: #{response.body}"
134
130
  fatal msg
135
131
  end
@@ -147,9 +143,6 @@ module Eco
147
143
  return nil if !data || !data.is_a?(Enumerable)
148
144
  fatal "cannot batch #{method} without api connnection, please provide a valid api connection!" unless people_api = api&.people
149
145
 
150
- # batch Status
151
- status = new_status(data, method)
152
-
153
146
  # param q does not make sense here, even for GET method
154
147
  params = {per_page: DEFAULT_BATCH_BLOCK}.merge(params)
155
148
  per_page = params[:per_page] || DEFAULT_BATCH_BLOCK
@@ -157,25 +150,26 @@ module Eco
157
150
  iteration = 1; done = 0
158
151
  iterations = (data.length.to_f / per_page).ceil
159
152
 
160
- data.each_slice(per_page) do |slice|
161
- msg = "starting batch '#{method}' iteration #{iteration}/#{iterations}, with #{slice.length} entries of #{data.length} -- #{done} done"
162
- logger.info(msg) unless silent
153
+ Eco::API::Session::Batch::Status.new(enviro, queue: data, method: method).tap do |status|
154
+ data.each_slice(per_page) do |slice|
155
+ msg = "starting batch '#{method}' iteration #{iteration}/#{iterations}, with #{slice.length} entries of #{data.length} -- #{done} done"
156
+ logger.info(msg) unless silent
163
157
 
164
- people_api.batch do |batch|
165
- slice.each do |person|
166
- batch.public_send(method, person) do |response|
167
- faltal("Request with no response") unless !!response
168
- status[person] = response
158
+ people_api.batch do |batch|
159
+ slice.each do |person|
160
+ batch.public_send(method, person) do |response|
161
+ faltal("Request with no response") unless !!response
162
+ status[person] = response
163
+ end
169
164
  end
170
- end
171
- end # next batch
165
+ end # next batch
172
166
 
173
- iteration += 1
174
- done += slice.length
175
- end # next slice
167
+ iteration += 1
168
+ done += slice.length
169
+ end # next slice
176
170
 
177
- status.errors.print unless silent
178
- return status
171
+ status.errors.print unless silent
172
+ end
179
173
  end
180
174
 
181
175
  end
@@ -184,6 +178,10 @@ module Eco
184
178
  end
185
179
 
186
180
  require_relative 'batch/job'
181
+ require_relative 'batch/feedback'
182
+ require_relative 'batch/request_stats'
183
+ require_relative 'batch/base_policy'
184
+ require_relative 'batch/policies'
187
185
  require_relative 'batch/status'
188
186
  require_relative 'batch/errors'
189
187
  require_relative 'batch/jobs'
@@ -0,0 +1,283 @@
1
+ module Eco
2
+ module API
3
+ class Session
4
+ class Batch
5
+ # Helper class to build a hiearchical model of policies
6
+ # @example Usage:
7
+ # class PolicyModel < Eco::API::Session::Batch::BasePolicy
8
+ # MODEL = {attr1: ["prop1a", "prop1b", {"prop1c": ["prop1c1"]}]}
9
+ # self.model = MODEL
10
+ # policy_attrs *model_attrs
11
+ # end
12
+ #
13
+ # policies = PolicyModel.new("batch_policy")
14
+ # policies.attr1c do |attr1c|
15
+ # attr1c.prop1c1.max = 30
16
+ # end
17
+ #
18
+ # @attr_reader attr [Symbol] the Symbol `name` of the current policy
19
+ # @attr_reader max [Integer] `max` **allowed** number of occurrences of the property
20
+ # @attr_reader min [Integer] `min` **required** number of occurrences of the property
21
+ class BasePolicy
22
+ extend Eco::API::Common::ClassHelpers
23
+
24
+ # @attr_reader model [Hash, nil] the `model` of the current `class`
25
+ class << self
26
+ attr_reader :model
27
+
28
+ # @param value [Hash, Enumerable, String, Symbol, nil] unparsed model to be assigned to the `class`
29
+ def model=(value)
30
+ @model = parse_model(value)
31
+ end
32
+
33
+ # @return [Array<String>] the `keys` of the current class' `model`
34
+ def model_attrs
35
+ (model && model.keys) || []
36
+ end
37
+
38
+ # Helper to normalize `key` into a correct `ruby` **constant name**
39
+ # @param key [String, Symbol] to be normalized
40
+ # @return [String] a correct constant name
41
+ def titleize(key)
42
+ str_name = key.to_s.strip.split(/[\-\_ ]/i).compact.map do |str|
43
+ str.slice(0).upcase + str.slice(1..-1).downcase
44
+ end.join("")
45
+ end
46
+
47
+ # If the class for `key` exists, it returns it. Otherwise it generates it.
48
+ # @note for this to work, `key` should be one of the submodels of the current class' `model`
49
+ # @return [Eco::API::Session::Batch::BasePolicy] or subclass thereof
50
+ def policy_class(key)
51
+ key = key.to_sym.freeze
52
+ class_name = titleize(key)
53
+ full_class_name = "#{self}::#{class_name}"
54
+
55
+ unless target_class = resolve_class(full_class_name, exception: false)
56
+ submodel = model[key]
57
+ target_class = Class.new(self) do |klass|
58
+ klass.model = submodel
59
+ policy_attrs *klass.model_attrs
60
+ end
61
+
62
+ self.const_set class_name, target_class
63
+ end
64
+
65
+ target_class
66
+ end
67
+
68
+ # Thanks to this step the format on the declaration of the model is flexible
69
+ # @param value [Hash, Enumerable, String, Symbol, nil]
70
+ # @return [Hash, nil] where keys are `Symbol` s
71
+ def parse_model(model)
72
+ case model
73
+ when String
74
+ return parse_model(model.to_sym)
75
+ when Symbol
76
+ return {model => nil}
77
+ when Hash
78
+ return model.each_with_object({}) do |(k,v), hash|
79
+ hash[k.to_sym] = v
80
+ end
81
+ when Enumerable
82
+ return model.each_with_object({}) do |sub, hash|
83
+ hash.merge!(parse_model(sub))
84
+ end
85
+ when NilClass
86
+ return nil
87
+ else
88
+ raise "Incorrect model declaration, allowed String, Symbol, Hash and Enumerable. Given: #{model.class}"
89
+ end
90
+ end
91
+
92
+ # Attributes of this level of the model that should be included
93
+ # @param attr [Symbol, String] each of the subpolicies of the model that should be available
94
+ def policy_attrs(*attrs)
95
+ attrs = attrs.map(&:to_sym)
96
+
97
+ attrs.each do |attr|
98
+ method = attr.to_s.freeze
99
+ var = "@#{method}".freeze
100
+
101
+ define_method(method) do |&block|
102
+
103
+ unless policy = self[attr]
104
+ klass = self.class.policy_class(attr)
105
+ policy = self[attr] = klass.new(attr, _parent: self)
106
+ end
107
+
108
+ if block
109
+ block.call(policy)
110
+ self
111
+ else
112
+ policy
113
+ end
114
+ end
115
+
116
+ end
117
+ end
118
+
119
+ end
120
+
121
+ include Enumerable
122
+
123
+ attr_reader :attr
124
+ attr_accessor :max, :min
125
+
126
+ def initialize(attr = nil, _parent: self)
127
+ @_parent = _parent
128
+ @attr = attr.to_sym
129
+ @policies = {}
130
+ end
131
+
132
+ def attr(as_namespace: false)
133
+ return @attr if !as_namespace || root?
134
+ "#{@_parent.attr(as_namespace: true)}:#{@attr}"
135
+ end
136
+
137
+ # @note if there's no `min` defined, it always returns `true`
138
+ # @param value [Integer] value to check if it's in the minimum required
139
+ # @retrun [Boolen] `true` if `value` is grater or equal to `min`
140
+ def min?(value)
141
+ !min || !value|| (min <= value)
142
+ end
143
+
144
+ # @note if there's no `max` defined, it always returns `true`
145
+ # @param value [Integer] value to check if it's in the maximum allowed
146
+ # @retrun [Boolen] `true` if `value` is lesser or equal to `min`
147
+ def max?(value)
148
+ !max || !value || (max >= value)
149
+ end
150
+
151
+ # return [Integer] number of declared subpolicies
152
+ def length
153
+ count
154
+ end
155
+
156
+ # @return [Boolean] `true` if there are no active subpolicies, `false` otherwise
157
+ def empty?
158
+ count == 0
159
+ end
160
+
161
+ # @return [Boolean] `true` if there are active subpolicies, `false` otherwise
162
+ def subpolicies?
163
+ !empty?
164
+ end
165
+
166
+ def each(params: {}, &block)
167
+ return to_enum(:each) unless block
168
+ items.each(&block)
169
+ end
170
+
171
+ # @return [Array<Eco::API::Session::Batch::BasePolicy>] the active subpolicies
172
+ def items
173
+ @policies.values
174
+ end
175
+
176
+ # @param attr [Symbol, String] name of the policy
177
+ # @return [Boolean] if `attr` is an active subpolicy
178
+ def active?(attr)
179
+ @policies.key?(attr.to_sym)
180
+ end
181
+
182
+ # @param attr [Symbol, String] name of the policy
183
+ # @return [Array<Eco::API::Session::Batch::BasePolicy>] the used subpolicies
184
+ def [](attr)
185
+ @policies[attr.to_sym]
186
+ end
187
+
188
+ # @param attr [Symbol, String] name of the policy
189
+ # @param value [Expected object of Eco::API::Session::Batch::BasePolicy] a subpolicy to assign to a name `attr`
190
+ def []=(attr, value)
191
+ raise "Expected object of Eco::API::Session::Batch::BasePolicy. Given #{value.class}" unless value.is_a?(Eco::API::Session::Batch::BasePolicy)
192
+ @policies[attr.to_sym] = value
193
+ end
194
+
195
+ # @param model [Hash] plain hash (or hashable object) with the stats to check policy compliance against
196
+ # @param recurse [Boolean] to determine if we only check the current policy or also all active subpolicies
197
+ # @return [Boolean] `true` if `model` is compliant with the current policy
198
+ def compliant?(model, recurse: true)
199
+ unless hash = model_to_hash(model)
200
+ raise "Expected 'model' to be a Hash (or hashable) object. Given: #{model}"
201
+ end
202
+ value = model_attr(hash)
203
+ good = !model_attr?(hash) || (min?(value) && max?(value))
204
+ #pp "batch_policy: '#{attr}' - #{value}: 'min' #{min?(value)}; 'max' #{max?(value)}"
205
+ good &&= all? {|active| active.compliant?(model, recurse: recurse)} if recurse
206
+ good
207
+ end
208
+
209
+ def validate!(model)
210
+ unless compliant?(model)
211
+ msg = self.uncompliance(model)
212
+ raise "Uncompliance Exception\n#{msg}"
213
+ end
214
+ end
215
+
216
+ # @param model [Hash] plain hash (or hashable object) with the stats to check policy compliance against
217
+ # @return [Array<Eco::API::Session::Batch::BasePolicy>] **non-compliant** policies for the `model`
218
+ def uncompliant(model)
219
+ each_with_object([]) do |active, arr|
220
+ arr.concat(active.uncompliant(model))
221
+ end.tap do |arr|
222
+ arr.unshift(self) unless compliant?(model, recurse: false)
223
+ end
224
+ end
225
+
226
+ # @param model [Hash] plain hash (or hashable object) with the stats to check policy compliance against
227
+ # @param recurse [Boolean] to determine if we only check the current policy or also all active subpolicies
228
+ # @return [String] message with what failed to meet compliance
229
+ def uncompliance(model, recurse: true)
230
+ unless hash = model_to_hash(model)
231
+ raise "Expected 'model' to be a Hash (or hashable) object. Given: #{model}"
232
+ end
233
+ msg = ""
234
+ unless compliant?(hash, recurse: false)
235
+ value = model_attr(hash)
236
+ msg += "'#{attr(as_namespace: true)}' fails to meet: "
237
+ msg += " [ min(#{min}) >= #{value}] " unless min?(value)
238
+ msg += " [ max(#{max}) <= #{value}] " unless max?(value)
239
+ msg += "\n"
240
+ end
241
+
242
+ if recurse
243
+ map do |active|
244
+ active.uncompliance(hash, recurse: true)
245
+ end.compact.tap do |msgs|
246
+ msg += "\n" + msgs.join("\n") unless msgs.empty?
247
+ end
248
+ end
249
+ msg
250
+ end
251
+
252
+ protected
253
+
254
+ # Internal helper to know if we are at the top/root of the hierarchical model
255
+ # @return [Boolean] `true` if this object is the top `root`, `false` otherwise
256
+ def root?
257
+ @_parent == self
258
+ end
259
+
260
+ def to_h
261
+ @policies
262
+ end
263
+
264
+ def model_attr?(hash)
265
+ hash.key?(self.attr) || hash.key?(self.attr.to_s)
266
+ end
267
+
268
+ def model_attr(hash)
269
+ hash[self.attr] || hash[self.attr.to_s] if model_attr?(hash)
270
+ end
271
+
272
+ private
273
+
274
+ def model_to_hash(model)
275
+ return model if model.is_a?(Hash)
276
+ model.to_h if model.respond_to?(:to_h)
277
+ end
278
+
279
+ end
280
+ end
281
+ end
282
+ end
283
+ end
@@ -2,27 +2,34 @@ module Eco
2
2
  module API
3
3
  class Session
4
4
  class Batch
5
+ # Helper object linked to a `batch status`
5
6
  class Errors
6
7
 
8
+ # @attr_reader status [Eco::API::Session::Batch::Status] `batch status` this `Errors` object is associated to.
7
9
  attr_reader :status
8
10
 
11
+ # @param status [Eco::API::Session::Batch::Status] `batch status` this `Errors` object is associated to.
9
12
  def initialize(status:)
10
- "Expected Batch::Status as root. Given: #{status.class}" unless status.is_a?(Batch::Status)
13
+ "Expected Batch::Status as root. Given: #{status.class}" unless status.is_a?(Eco::API::Session::Batch::Status)
11
14
  @status = status
12
15
  end
13
16
 
17
+ # @see [Eco::API::Session::Batch::Status#queue]
14
18
  def queue
15
19
  status.queue
16
20
  end
17
21
 
22
+ # @see [Eco::API::Session::Batch::Status#method]
18
23
  def method
19
24
  status.method
20
25
  end
21
26
 
27
+ # @see [Eco::API::Session::Batch::Status#to_index]
22
28
  def to_index(*args)
23
29
  status.to_index(*args)
24
30
  end
25
31
 
32
+ # @return [Eco::API::Session] currently active `session`
26
33
  def session
27
34
  status.session
28
35
  end
@@ -31,13 +38,20 @@ module Eco
31
38
  status.logger
32
39
  end
33
40
 
41
+ # Was there any **error** as a result of this batch?
42
+ # @return [Boolean] `true` if any of the queried _entries_ got an unsuccessful `Ecoportal::API::Common::BatchResponse`
34
43
  def any?
35
44
  queue.any? {|query| !status[query].success?}
36
45
  end
37
46
 
47
+ # Input entries that got launched against the server.
48
+ # @raise [Exception] if there are elements of the final `queue` that did not get response
49
+ # @note discards those that did not get _response_ from the Server (so those that were not queried)
50
+ # - please, observe that this can only happen if there were repeated entries in the `source_queue`
51
+ # @return [Array<Hash>, Array<Ecoportal::API::V1::Person>, Array<Ecoportal::API::Internal::Person>]
38
52
  def entries
39
53
  queue.each_with_index.map do |query, i|
40
- unless status[i]
54
+ unless response = status[i]
41
55
  msg = "Error: query with no response. You might have duplicated entries in your queue.\n"
42
56
  msg += "Queue length: #{queue.length}; Queue elements class: #{queue.first.class}\n"
43
57
  msg += "Query with no response. Person: #{person_ref(query)}\n"
@@ -49,7 +63,7 @@ module Eco
49
63
  raise msg
50
64
  end
51
65
 
52
- status[i].success? ? nil : query
66
+ response.success? ? nil : query
53
67
  end.compact
54
68
  end
55
69
 
@@ -0,0 +1,112 @@
1
+ module Eco
2
+ module API
3
+ class Session
4
+ class Batch
5
+ # @attr_reader job [Eco::API::Session::Batch::Job] `batch job` the feedback is associated with
6
+ class Feedback
7
+
8
+ attr_reader :job
9
+
10
+ # @param job [Eco::API::Session::Batch::Job] `batch job` the feedback is associated with
11
+ def initialize(job:)
12
+ raise "A Eco::API::Session::Batch::Job object is required. Given: #{job}" unless job.is_a?(Eco::API::Session::Batch::Job)
13
+ @job = job
14
+ end
15
+
16
+ # @see Eco::API::Session::Batch::Job#name
17
+ # @return [String] name of the `batch job`
18
+ def name
19
+ job.name
20
+ end
21
+
22
+ # @see Eco::API::Session::Batch::Job#type
23
+ def type
24
+ job.type
25
+ end
26
+
27
+ # @see Eco::API::Session::Batch::Job#sets
28
+ def sets
29
+ job.sets
30
+ end
31
+
32
+ # @see Eco::API::Session::Batch::Job#options
33
+ def options
34
+ job.options
35
+ end
36
+
37
+ # Slightly modifies the behaviour of `as_update`, so schema detail fields show the `alt_id`
38
+ # @note for better feedback
39
+ # @param entry [Hash, Ecoportal::API::V1::Person, Ecoportal::API::Internal::Person]
40
+ def as_update(entry)
41
+ case
42
+ when entry.is_a?(Hash)
43
+ hash = entry
44
+ else #entry.is_a?(Ecoportal::API::V1::Person)
45
+ if only_ids?
46
+ hash = {
47
+ "id" => entry.id,
48
+ "external_id" => entry.external_id,
49
+ "email" => entry.email
50
+ }
51
+ hash = entry.as_json.slice("id", "external_id", "email")
52
+ else
53
+ hash = entry.as_update
54
+ if entry.details
55
+ if hfields = hash.dig("details", "fields")
56
+ hfields.each do |fld|
57
+ fld.merge!("alt_id" => entry.details.get_field(fld["id"]).alt_id)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ hash || {}
64
+ end
65
+
66
+ def request_stats(data)
67
+ @request_stats ||= Eco::API::Session::Batch::RequestStats.new(type: type, requests: data)
68
+ end
69
+
70
+ def generate(requests, max_chars: 800, only_stats: false)
71
+ msg = []
72
+ if !requests || !requests.is_a?(Enumerable) || requests.empty?
73
+ msg << "#{"*" * 20} Nothing for #{signature} so far :) #{"*" * 20}"
74
+ else
75
+ sample_length = 1
76
+ sample = requests.slice(0, 20).map do |request|
77
+ max_chars -= request.pretty_inspect.length
78
+ sample_length += 1 if max_chars > 0
79
+ request
80
+ end
81
+
82
+ header = "#{"*"*20} #{signature} #{only_stats ? "" : "- Feedback Sample"} #{"*"*20}"
83
+ msg << header
84
+ unless only_stats
85
+ msg << "#{sample.slice(0, sample_length).pretty_inspect}"
86
+ end
87
+ msg << "#{"+"*5} STATS ++ #{type.to_s.upcase} length: #{requests.length} #{"+"*5}"
88
+ msg << "#{request_stats(requests).message}"
89
+ msg << "*" * header.length
90
+ end
91
+ msg.join("\n")
92
+ end
93
+
94
+ private
95
+
96
+ def only_ids?
97
+ [:delete, :get].include?(type)
98
+ end
99
+
100
+ def signature
101
+ "Batch job \"#{name}\" ['#{type.to_s.upcase}': #{sets_title}]"
102
+ end
103
+
104
+ def sets_title
105
+ "#{sets.map {|s| s.to_s}.join(", ")}"
106
+ end
107
+
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end