eco-helpers 1.0.13 → 1.0.14

Sign up to get free protection for your applications and to get access to all the features.
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