eco-helpers 3.0.16 → 3.0.17

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: baeb42abbb1d31497328afd4c813dec4acfebb598546ad55a4f1265a45dc89b5
4
- data.tar.gz: 7d4c88626946b84e1ea22f3b27eb7570a9daa6d2962036e6fbf667b000dd5996
3
+ metadata.gz: 884bc5e19544453088719de4fb1445b7fdb38de4b14b0a70d2dbe795d7082fb6
4
+ data.tar.gz: 0ba7fa5b2367d5fe51e7a51571c5e031961ddb94ec863fa71a6ab2869aa7a353
5
5
  SHA512:
6
- metadata.gz: 8af8fdbccacb57655756a01f0c30ff1d38c58e232f77fedc5aeb058cb7260ed1863834af1995d7268d355e09c00fedc671b4417f1f14d76c25c9609a038b47d3
7
- data.tar.gz: c4daf6291086d8b15f5eb1606291e8161e0e0326b4f5ed8e917e95d09b9c1de7ff2665e44ec3ed35986d0cd07ec232dfe40822d0d32bbd12020f7290a9ce1405
6
+ metadata.gz: 75eddb8557bf95b1174a2de8d18922aeac5bb3fb74fa8371c58d8c74be0413d7eef12a682549efe4a996c5413bc245e75dc2a6b06d9615aee520ab3be3247abd
7
+ data.tar.gz: 898c76f69fd73fd89c261ff4989798c2dec9d845ce8b4aadb168ac1c44c0b7c67cd0d6a1aabead337ede761ef37dcf7d850e895057d04af211c8f19a6708cf94
data/CHANGELOG.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [3.0.17] - 2024-10-xx
5
+ ## [3.0.18] - 2024-10-xx
6
6
 
7
7
  ### Added
8
8
 
@@ -10,10 +10,19 @@ All notable changes to this project will be documented in this file.
10
10
 
11
11
  ### Fixed
12
12
 
13
- ## [3.0.16] - 2024-10-15
13
+ ## [3.0.17] - 2024-10-18
14
14
 
15
15
  ### Added
16
16
 
17
+ - Configurability around the **size** of batch and job modes
18
+
19
+ ### Changed
20
+
21
+ - `ecoportal-api` gem upgrade
22
+ - Internal refactor of `Eco::API::Session::Batch`
23
+
24
+ ## [3.0.16] - 2024-10-15
25
+
17
26
  ### Changed
18
27
 
19
28
  - `Eco::API::Session::Batch#launch_batch`
data/eco-helpers.gemspec CHANGED
@@ -40,7 +40,7 @@ Gem::Specification.new do |spec|
40
40
  spec.add_dependency 'bcrypt_pbkdf', '~> 1.0'
41
41
  spec.add_dependency 'docx', '>= 0.8.0', '< 0.9'
42
42
  spec.add_dependency 'dotenv', '~> 3'
43
- spec.add_dependency 'ecoportal-api', '~> 0.10', '>= 0.10.4'
43
+ spec.add_dependency 'ecoportal-api', '~> 0.10', '>= 0.10.5'
44
44
  spec.add_dependency 'ecoportal-api-graphql', '~> 0.4', '>= 0.4.2'
45
45
  spec.add_dependency 'ecoportal-api-v2', '~> 2.0', '>= 2.0.10'
46
46
  spec.add_dependency 'ed25519', '~> 1.2'
@@ -0,0 +1,23 @@
1
+ module Eco
2
+ module API
3
+ class Session
4
+ class Batch
5
+ module Launcher
6
+ module Benchmarking
7
+ private
8
+
9
+ def str_per_sec(start, count)
10
+ now = Time.now
11
+ secs = (now - start).round(2)
12
+
13
+ return ' -- ' unless secs > 0.0
14
+
15
+ per_sec = (count.to_f / secs).round(2)
16
+ "#{secs}s -> #{per_sec} people/s"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Eco
2
+ module API
3
+ class Session
4
+ class Batch
5
+ module Launcher
6
+ module Mode
7
+ include Eco::API::Session::Batch::Launcher::Options
8
+
9
+ # @return [Symbol] the batch mode to run
10
+ def batch_mode(opts = options)
11
+ opts.dig(:workflow, :batch, :mode) || :batch
12
+ end
13
+
14
+ # @return [Boolean] are we running in `:job` mode?
15
+ def job_mode?(opts = options)
16
+ batch_mode(opts) == :job
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ module Eco
2
+ module API
3
+ class Session
4
+ class Batch
5
+ module Launcher
6
+ module Options
7
+ private
8
+
9
+ # Default way to retrieve options (unless provided)
10
+ def options
11
+ return @options if instance_variable_defined?(:@options)
12
+ return super if defined?(super)
13
+
14
+ ASSETS.cli.options
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,40 @@
1
+ module Eco
2
+ module API
3
+ class Session
4
+ class Batch
5
+ module Launcher
6
+ module Retry
7
+ def self.included(base)
8
+ unless base <= Eco::API::Session::Batch::Launcher
9
+ msg = "To be included only in Eco::API::Common::Session::BaseSession. "
10
+ msg << "Tried on '#{base}'"
11
+ raise msg
12
+ end
13
+
14
+ super
15
+ end
16
+
17
+ private
18
+
19
+ def offer_retry_on(error_type, retries_left = 3, &block)
20
+ yield
21
+ rescue error_type => err
22
+ raise err.class, err.message, cause: nil unless retries_left.positive?
23
+
24
+ explanation = "#{err}\n"
25
+ explanation << "You have #{retries_left} retries left."
26
+ question = " Do you want to retry (y/N)?"
27
+
28
+ prompt_user(question, default: "Y", explanation: explanation, timeout: 10) do |response|
29
+ raise unless response.upcase.start_with?("Y")
30
+
31
+ puts "\nOkay... let's retry!"
32
+ offer_retry_on(error_type, retries_left - 1, &block)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,40 @@
1
+ module Eco
2
+ module API
3
+ class Session
4
+ class Batch
5
+ module Launcher
6
+ module Size
7
+ include Eco::API::Session::Batch::Launcher::Mode
8
+
9
+ DEFAULT_BATCH_SIZE = 50
10
+ DEFAULT_JOB_SIZE = 100
11
+
12
+ def batch_size(opts = options)
13
+ return job_mode_size if job_mode?(opts)
14
+
15
+ batch_mode_size
16
+ end
17
+
18
+ private
19
+
20
+ def job_mode_size
21
+ options.dig(:workflow, :batch, :job, :size).then do |size|
22
+ next self.class::DEFAULT_JOB_SIZE unless size
23
+
24
+ size
25
+ end
26
+ end
27
+
28
+ def batch_mode_size
29
+ options.dig(:workflow, :batch, :size).then do |size|
30
+ next self.class::DEFAULT_BATCH_SIZE unless size
31
+
32
+ [size, 100].min
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,23 @@
1
+ module Eco
2
+ module API
3
+ class Session
4
+ class Batch
5
+ module Launcher
6
+ module StatusHandling
7
+ private
8
+
9
+ def tap_status(enviro:, queue:, method:, status: nil, &block)
10
+ status ||= Eco::API::Session::Batch::Status.new(
11
+ enviro,
12
+ queue: queue,
13
+ method: method
14
+ )
15
+
16
+ status.tap(&block)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ module Eco
2
+ module API
3
+ class Session
4
+ class Batch
5
+ module Launcher
6
+ module ValidMethods
7
+ VALID_METHODS = %i[get create update upsert delete].freeze
8
+
9
+ def self.included(base)
10
+ super
11
+ base.extend(ClassMethods)
12
+ base.send(:include, InstanceMethods)
13
+ end
14
+
15
+ module ClassMethods
16
+ # @return [Boolean] `true` if the method is supported, `false` otherwise.
17
+ def valid_method?(value)
18
+ VALID_METHODS.include?(value)
19
+ end
20
+ end
21
+
22
+ module InstanceMethods
23
+ private
24
+
25
+ def valid_method?(value)
26
+ self.class.valid_method?(value)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,141 @@
1
+ require_relative 'launcher/valid_methods'
2
+ require_relative 'launcher/options'
3
+ require_relative 'launcher/mode'
4
+ require_relative 'launcher/size'
5
+ require_relative 'launcher/benchmarking'
6
+ require_relative 'launcher/status_handling'
7
+ require_relative 'launcher/retry'
8
+
9
+ module Eco
10
+ module API
11
+ class Session
12
+ class Batch
13
+ module Launcher
14
+ def self.included(base)
15
+ unless base <= Eco::API::Common::Session::BaseSession
16
+ msg = "To be included only in Eco::API::Common::Session::BaseSession. "
17
+ msg << "Tried on '#{base}'"
18
+ raise msg
19
+ end
20
+
21
+ super
22
+
23
+ base.send(:include, ValidMethods)
24
+ end
25
+
26
+ include Options
27
+ include Mode
28
+ include Size
29
+ include Benchmarking
30
+ include StatusHandling
31
+ include Retry
32
+
33
+ private
34
+
35
+ def batch_from(
36
+ data,
37
+ method:,
38
+ params: {},
39
+ silent: false,
40
+ options: self.options
41
+ )
42
+ fatal "Invalid batch method: #{method}." unless valid_method?(method)
43
+ return unless data.is_a?(Enumerable)
44
+
45
+ msg = "cannot batch #{method} without api connnection, please provide a valid api connection!"
46
+ fatal msg unless (people_api = api&.people)
47
+
48
+ launch_batch(
49
+ data,
50
+ method: method,
51
+ per_page: params[:per_page] || batch_size(options),
52
+ people_api: people_api,
53
+ silent: silent,
54
+ options: options
55
+ )
56
+ end
57
+
58
+ def launch_batch( # rubocop:disable Metrics/AbcSize
59
+ data,
60
+ method:,
61
+ status: nil,
62
+ job_mode: true, # rubocop:disable Lint/UnusedMethodArgument
63
+ options: self.options,
64
+ per_page: batch_size(options),
65
+ people_api: api&.people,
66
+ silent: false
67
+ )
68
+ iteration = 1
69
+ done = 0
70
+ iterations = (data.length.to_f / per_page).ceil
71
+
72
+ tap_status(status: status, enviro: enviro, queue: data, method: method) do |overall_status|
73
+ pending_for_server_error = data.to_a[0..]
74
+
75
+ start_time = Time.now
76
+
77
+ data.each_slice(per_page) do |slice|
78
+ msg = "starting batch '#{method}' iteration #{iteration}/#{iterations}, "
79
+ msg << "with #{slice.length} entries of #{data.length} -- #{done} done"
80
+ msg << (" " * 20)
81
+ log(:info) { msg } unless silent
82
+
83
+ start_slice = Time.now
84
+
85
+ offer_retry_on(Ecoportal::API::Errors::TimeOut) do
86
+ people_api.batch(job_mode: job_mode?(options)) do |batch|
87
+ slice.each do |person|
88
+ batch.public_send(method, person) do |response|
89
+ faltal("Request with no response") unless response
90
+
91
+ next if server_error?(response)
92
+
93
+ pending_for_server_error.delete(person)
94
+ overall_status[person] = response
95
+ end
96
+ end
97
+ end # end batch
98
+ end
99
+
100
+ done += slice.length
101
+
102
+ msg = " ... iteration #{iteration}/#{iterations} done "
103
+ msg << "in #{str_per_sec(start_slice, slice.length)} "
104
+ msg << "(average: #{str_per_sec(start_time, done)})"
105
+ msg << (" " * 20)
106
+ log(:info) { msg } unless silent
107
+
108
+ iteration += 1
109
+ end # next slice
110
+
111
+ # temporary working around (due to back-end problems with batch/jobs)
112
+ unless pending_for_server_error.empty?
113
+ msg = "Going to re-try #{pending_for_server_error.count} due to server errors"
114
+ log(:info) { msg } unless silent
115
+
116
+ launch_batch(
117
+ pending_for_server_error,
118
+ status: overall_status,
119
+ method: method,
120
+ job_mode: false,
121
+ per_page: per_page,
122
+ people_api: people_api,
123
+ silent: silent,
124
+ options: options
125
+ )
126
+ end
127
+ end
128
+ end
129
+
130
+ def server_error?(response)
131
+ res_status = response.status
132
+ server_error = !res_status || res_status.server_error?
133
+ other_error = !server_error && (!res_status.code || res_status.code < 100)
134
+ no_body = !server_error && !other_error && !response.body
135
+ server_error || other_error || no_body
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -6,48 +6,48 @@ module Eco
6
6
  # @attr_reader stats [Hash] plain `Hash` with the number of requests that include an attribute
7
7
  class RequestStats
8
8
  CORE_ATTRS = Eco::API::Common::People::PersonParser::CORE_ATTRS
9
- ACCOUNT_ATTRS = (Eco::API::Common::People::PersonParser::ACCOUNT_ATTRS + ["permissions_custom"]).uniq
10
- DETAILS_ATTRS = ["fields"]
11
- BLANKED_PREFIX = "blanked_"
12
- DETAILS_FIELDS = "details_fields"
9
+ ACCOUNT_ATTRS = (Eco::API::Common::People::PersonParser::ACCOUNT_ATTRS + ['permissions_custom']).uniq
10
+ DETAILS_ATTRS = ['fields'].freeze
11
+ BLANKED_PREFIX = 'blanked_'.freeze
12
+ DETAILS_FIELDS = 'details_fields'.freeze
13
13
 
14
14
  class << self
15
-
16
15
  def valid_type?(type)
17
16
  Eco::API::Session::Batch::Job.valid_type?(type.to_sym)
18
17
  end
19
18
 
20
19
  def core_attrs(stats: false, all: false)
21
20
  CORE_ATTRS.dup.tap do |attrs|
22
- if stats || all
23
- attrs.unshift("core")
24
- attrs.concat(blank_attrs(CORE_ATTRS))
25
- end
21
+ next unless stats || all
22
+
23
+ attrs.unshift('core')
24
+ attrs.concat(blank_attrs(CORE_ATTRS))
26
25
  end
27
26
  end
28
27
 
29
28
  def account_attrs(stats: false, all: false)
30
29
  ACCOUNT_ATTRS.dup.tap do |attrs|
31
- if stats || all
32
- attrs.unshift("account_remove")
33
- attrs.unshift("account") if all
34
- attrs.concat(blank_attrs(ACCOUNT_ATTRS))
35
- end
30
+ next unless stats || all
31
+
32
+ attrs.unshift('account_remove')
33
+ attrs.unshift('account') if all
34
+ attrs.concat(blank_attrs(ACCOUNT_ATTRS))
36
35
  end
37
36
  end
38
37
 
39
38
  def details_attrs(stats: false, all: false)
40
39
  DETAILS_ATTRS.dup.tap do |attrs|
41
- if stats || all
42
- attrs.unshift("details_remove")
43
- attrs.unshift("details") if all
44
- end
40
+ next unless stats || all
41
+
42
+ attrs.unshift('details_remove')
43
+ attrs.unshift('details') if all
45
44
  end
46
45
  end
47
46
 
48
47
  def blanked_prefix(attr = nil)
49
48
  @blanked_prefix ||= BLANKED_PREFIX
50
49
  return @blanked_prefix unless attr
50
+
51
51
  "#{blanked_prefix}#{attr}"
52
52
  end
53
53
 
@@ -58,15 +58,16 @@ module Eco
58
58
  def blank_attrs(attrs)
59
59
  attrs.map {|attr| "#{blanked_prefix}#{attr}"}
60
60
  end
61
-
62
61
  end
63
62
 
64
63
  attr_reader :type, :count
65
64
 
66
65
  def initialize(type:, requests: [])
67
- raise "type should be one of #{Eco::API::Session::Batch::Job.types}. Given: #{type}" unless self.class.valid_type?(type.to_sym)
66
+ msg = "Type should be one of #{Eco::API::Session::Batch::Job.types}. Given: #{type}"
67
+ raise msg unless self.class.valid_type?(type.to_sym)
68
+
68
69
  @type = type.to_sym
69
- @count = requests && requests.length
70
+ @count = requests&.length
70
71
  @stats = build(requests)
71
72
  end
72
73
 
@@ -86,21 +87,22 @@ module Eco
86
87
 
87
88
  def attr_value(attr, percent: false, total: count, details: false)
88
89
  target = details ? (@stats[DETAILS_FIELDS] || {}) : @stats
89
- i = target["#{attr}"]
90
+ i = target[attr.to_s]
90
91
  return i unless percent
92
+
91
93
  percentage(i, total: total)
92
94
  end
93
95
 
94
96
  def core(percent: false)
95
- attr_value("core", percent: percent)
97
+ attr_value('core', percent: percent)
96
98
  end
97
99
 
98
100
  def account(percent: false)
99
- attr_value("account", percent: percent)
101
+ attr_value('account', percent: percent)
100
102
  end
101
103
 
102
104
  def details(percent: false)
103
- attr_value("details", percent: percent)
105
+ attr_value('details', percent: percent)
104
106
  end
105
107
 
106
108
  def stats
@@ -110,13 +112,20 @@ module Eco
110
112
  def build(requests)
111
113
  stats.tap do |stats|
112
114
  stats[type] = count
113
- unless !requests || !requests.is_a?(Enumerable) || requests.empty?
114
- stats[DETAILS_FIELDS] = Hash.new(0)
115
- requests.each_with_index do |request|
116
- add_core_stats(stats, request || {})
117
- add_account_stats(stats, request || {})
118
- add_details_stats(stats, request || {})
119
- end
115
+
116
+ no_requests =
117
+ !requests ||
118
+ !requests.is_a?(Enumerable) ||
119
+ requests.empty?
120
+
121
+ next if no_requests
122
+
123
+ stats[DETAILS_FIELDS] = Hash.new(0)
124
+
125
+ requests.each do |request|
126
+ add_core_stats(stats, request || {})
127
+ add_account_stats(stats, request || {})
128
+ add_details_stats(stats, request || {})
120
129
  end
121
130
  end
122
131
  end
@@ -124,74 +133,84 @@ module Eco
124
133
  def attrs_to_stat(stats, hash, attrs)
125
134
  stats.tap do |st|
126
135
  attrs.each do |attr|
127
- if hash.key?(attr)
128
- st[attr] += 1
129
- st[blanked_prefix(attr)]+= 1 if blanked_value?(hash[attr])
130
- end
136
+ next unless hash.key?(attr)
137
+
138
+ st[attr] += 1
139
+ next unless blanked_value?(hash[attr])
140
+
141
+ st[blanked_prefix(attr)]+= 1
131
142
  end
132
143
  end
133
144
  end
134
145
 
135
146
  def add_core_stats(stats, request)
136
- stats["core"] += 1 if (request.keys & core_attrs).length > 0
147
+ any_core = !(request.keys & core_attrs).empty? # rubocop:disable Style/ArrayIntersect
148
+ stats['core'] += 1 if any_core
149
+
137
150
  attrs_to_stat(stats, request, core_attrs)
138
151
  end
139
152
 
140
153
  def add_account_stats(stats, request)
141
- if request.key?("account")
142
- stats["account"] += 1
143
- stats["account_remove"] += 1 if !request["account"]
144
- attrs_to_stat(stats, request["account"] || {}, account_attrs)
145
- end
154
+ return unless request.key?('account')
155
+
156
+ stats['account'] += 1
157
+ stats['account_remove'] += 1 unless request['account']
158
+
159
+ attrs_to_stat(stats, request['account'] || {}, account_attrs)
146
160
  end
147
161
 
148
162
  def add_details_stats(stats, request)
149
- if request.key?("details")
150
- stats["details"] += 1
151
- stats["details_remove"] += 1 if !request["details"]
152
-
153
- det_attrs = {}
154
- if fields = request.dig("details", "fields")
155
- stats["fields"] += fields.length
156
- det_attrs = fields.each_with_object(det_attrs) {|fld, hash| hash[fld["alt_id"]] = fld["value"]}
157
- end
163
+ return unless request.key?('details')
164
+
165
+ stats['details'] += 1
166
+ stats['details_remove'] += 1 unless request['details']
167
+
168
+ det_attrs = {}
169
+ if (fields = request.dig('details', 'fields'))
170
+ stats['fields'] += fields.length
158
171
 
159
- attrs_to_stat(stats[DETAILS_FIELDS], det_attrs, det_attrs.keys)
172
+ det_attrs = fields.each_with_object(det_attrs) do |fld, hash|
173
+ hash[fld['alt_id']] = fld['value']
174
+ end
160
175
  end
176
+
177
+ attrs_to_stat(stats[DETAILS_FIELDS], det_attrs, det_attrs.keys)
161
178
  end
162
179
 
163
180
  def core_pairs(percent: false)
164
- cattrs = core_attrs + blank_attrs(core_attrs)
165
- [["core", core(percent: percent)]] + pairs(cattrs, percent: percent, total: core)
181
+ cattrs = core_attrs + blank_attrs(core_attrs)
182
+ [['core', core(percent: percent)]] + pairs(cattrs, percent: percent, total: core)
166
183
  end
167
184
 
168
185
  def account_pairs(percent: false)
169
- aattrs = ["account_remove"] + account_attrs + blank_attrs(account_attrs)
170
- [["account", account(percent: percent)]] + pairs(aattrs, percent: percent, total: account)
186
+ aattrs = ['account_remove'] + account_attrs + blank_attrs(account_attrs)
187
+ [['account', account(percent: percent)]] + pairs(aattrs, percent: percent, total: account)
171
188
  end
172
189
 
173
190
  def details_pairs(percent: false)
174
- det_pairs = [["details", details(percent: percent)]]
175
- det_pairs += [["fields", fields_average]] if attr_value("fields") && fields_average
176
- det_pairs += pairs(["details_remove"], percent: percent, total: details)
191
+ det_pairs = [['details', details(percent: percent)]]
192
+ det_pairs += [['fields', fields_average]] if attr_value('fields') && fields_average
193
+ det_pairs += pairs(['details_remove'], percent: percent, total: details)
177
194
  det_pairs += pairs(details_field_attrs, percent: percent, total: details, details: true)
195
+ det_pairs
178
196
  end
179
197
 
180
198
  def pairs(attrs, percent: false, total: count, details: false)
181
- pairs = attrs.map do |a|
182
- (v = attr_value(a, percent: percent, total: count, details: details)) > 0 ? [a, v] : nil
199
+ attrs.map do |a|
200
+ value = attr_value(a, percent: percent, total: total, details: details)
201
+ value.positive? ? [a, value] : nil
183
202
  end.compact
184
203
  end
185
204
 
186
205
  def pairs_to_line(pairs, percent: false)
187
- key_val_delimiter = ": "; attr_delimiter = " ++ "
206
+ key_val_delimiter = ': '; attr_delimiter = ' ++ '
188
207
  pairs.map do |p|
189
- [p.first.to_s, "#{p.last.to_s}" + (percent ? "%" : "")].join(key_val_delimiter)
208
+ [p.first.to_s, "#{p.last}#{percent ? '%' : ''}"].join(key_val_delimiter)
190
209
  end.join(attr_delimiter)
191
210
  end
192
211
 
193
212
  def core_attrs
194
- @core_attrs ||= self.class.core_attrs
213
+ @core_attrs ||= self.class.core_attrs
195
214
  end
196
215
 
197
216
  def account_attrs
@@ -216,12 +235,10 @@ module Eco
216
235
 
217
236
  def blanked_value?(value)
218
237
  case value
219
- when nil
220
- true
221
- when false
238
+ when nil, false
222
239
  true
223
240
  when Numeric
224
- value == 0
241
+ value.zero?
225
242
  when Array
226
243
  value.compact.empty?
227
244
  when String
@@ -230,16 +247,17 @@ module Eco
230
247
  end
231
248
 
232
249
  def fields_average
233
- if (fields_num = attr_value("fields")) && (total = details) > 0
234
- (fields_num.to_f / total.to_f).round(2)
235
- end
250
+ fields_num = attr_value('fields')
251
+ return unless fields_num && (total = details).positive?
252
+
253
+ (fields_num / total.to_f).round(2)
236
254
  end
237
255
 
238
256
  def percentage(num, total: count)
257
+ return unless num
258
+
239
259
  total ||= count
240
- if num
241
- (num.to_f / total * 100).round(2)
242
- end
260
+ (num.to_f / total * 100).round(2)
243
261
  end
244
262
  end
245
263
  end
@@ -0,0 +1,108 @@
1
+ require_relative 'launcher/valid_methods'
2
+ require_relative 'launcher/options'
3
+
4
+ module Eco
5
+ module API
6
+ class Session
7
+ class Batch
8
+ module Searcher
9
+ def self.included(base)
10
+ unless base <= Eco::API::Session::Batch
11
+ msg = "To be included only in Eco::API::Common::Session::BaseSession. "
12
+ msg << "Tried on '#{base}'"
13
+ raise msg
14
+ end
15
+
16
+ super
17
+ end
18
+
19
+ # Gets the _people_ of the organization according `params`.
20
+ # If `people` is not `nil`, scopes to only the people specified.
21
+ # @note
22
+ # - If `people` is given keys `page:` and `q` of `params:`.
23
+ # @param people [Nil, People, Enumerable<Person>, Enumerable<Hash>] target _People_ to launch the batch against.
24
+ # @param params [Hash] api request options.
25
+ # @option params [String] :page the page number `page` based on `:per_page`.
26
+ # @option params [String] :per_page the number of people included per each batch api request.
27
+ # @option params [String] :q some text to search. Omit this parameter to target all the people.
28
+ # @return [Array<People>] all the people based on `params`
29
+ def get_people(people = nil, params: {}, silent: false, options: self.options)
30
+ return get(params: params, silent: silent, options: options) unless people.is_a?(Enumerable)
31
+
32
+ launch(
33
+ people,
34
+ method: :get,
35
+ params: params,
36
+ silent: silent,
37
+ options: options
38
+ ).people
39
+ end
40
+
41
+ def search(data, silent: false, params: {}, options: self.options) # rubocop:disable Metrics/AbcSize
42
+ params = {per_page: batch_size(options)}.merge(params)
43
+
44
+ launch(
45
+ data,
46
+ method: :get,
47
+ params: params,
48
+ silent: silent,
49
+ options: options
50
+ ).tap do |status|
51
+ status.mode = :search
52
+
53
+ entries = status.queue
54
+ puts "\n"
55
+
56
+ entries.each_with_index do |entry, i|
57
+ if (i % 10).zero?
58
+ percent = i * 100 / entries.length
59
+ print "Searching: #{percent.round}% (#{i}/#{entries.length} entries)\r"
60
+ $stdout.flush
61
+ end
62
+
63
+ next if status.success?(entry)
64
+
65
+ email = nil
66
+ if entry.respond_to?(:email)
67
+ email = entry.email
68
+ elsif entry.respond_to?(:to_h)
69
+ email = entry.to_h["email"]
70
+ end
71
+
72
+ people_matching = []
73
+ email = email.to_s.strip.downcase
74
+
75
+ unless email.empty?
76
+ people_matching = get(
77
+ params: params.merge(q: email),
78
+ silent: silent,
79
+ options: options
80
+ ).select do |person|
81
+ person.email == email
82
+ end
83
+ end
84
+
85
+ case people_matching.length
86
+ when 1
87
+ status.set_person_match(entry, people_matching.first)
88
+ when 2..Float::INFINITY
89
+ status.set_people_match(entry, people_matching)
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def get(params: {}, silent: false, options: self.options)
98
+ msg = "cannot batch get without api connnection, please provide a valid api connection!"
99
+ fatal msg unless (people_api = api&.people)
100
+
101
+ params = {per_page: batch_size(options)}.merge(params)
102
+ people_api.get_all(params: params, silent: silent)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -2,54 +2,11 @@ module Eco
2
2
  module API
3
3
  class Session
4
4
  class Batch < Common::Session::BaseSession
5
- DEFAULT_BATCH_SIZE = 50
6
- DEFAULT_JOB_SIZE = 100
7
- VALID_METHODS = %i[get create update upsert delete].freeze
5
+ require_relative 'batch/launcher'
6
+ require_relative 'batch/searcher'
8
7
 
9
- class << self
10
- # @return [Boolean] `true` if the method is supported, `false` otherwise.
11
- def valid_method?(value)
12
- VALID_METHODS.include?(value)
13
- end
14
- end
15
-
16
- def batch_size(opts = options)
17
- return self.class::DEFAULT_JOB_SIZE if job_mode?(opts)
18
-
19
- self.class::DEFAULT_BATCH_SIZE
20
- end
21
-
22
- # @return [Symbol] the batch mode to run
23
- def batch_mode(opts = options)
24
- opts.dig(:workflow, :batch, :mode) || :batch
25
- end
26
-
27
- # @return [Boolean] are we running in `:job` mode?
28
- def job_mode?(opts = options)
29
- batch_mode(opts) == :job
30
- end
31
-
32
- # Gets the _people_ of the organization according `params`.
33
- # If `people` is not `nil`, scopes to only the people specified.
34
- # @note
35
- # - If `people` is given keys `page:` and `q` of `params:`.
36
- # @param people [Nil, People, Enumerable<Person>, Enumerable<Hash>] target _People_ to launch the batch against.
37
- # @param params [Hash] api request options.
38
- # @option params [String] :page the page number `page` based on `:per_page`.
39
- # @option params [String] :per_page the number of people included per each batch api request.
40
- # @option params [String] :q some text to search. Omit this parameter to target all the people.
41
- # @return [Array<People>] all the people based on `params`
42
- def get_people(people = nil, params: {}, silent: false, options: self.options)
43
- return get(params: params, silent: silent, options: options) unless people.is_a?(Enumerable)
44
-
45
- launch(
46
- people,
47
- method: :get,
48
- params: params,
49
- silent: silent,
50
- options: options
51
- ).people
52
- end
8
+ include Launcher
9
+ include Searcher
53
10
 
54
11
  # launches a batch of `method` type using `people` and the specified `params`
55
12
  # @raise Exception
@@ -60,222 +17,8 @@ module Eco
60
17
  # @param params [Hash] api request options.
61
18
  # @option params [String] :per_page the number of people included per each batch api request.
62
19
  # @return [Batch::Status] the `status` of this batch launch.
63
- def launch(people, method:, params: {}, silent: false, options: self.options)
64
- batch_from(
65
- people,
66
- method: method,
67
- params: params,
68
- silent: silent,
69
- options: options
70
- )
71
- end
72
-
73
- def search(data, silent: false, params: {}, options: self.options) # rubocop:disable Metrics/AbcSize
74
- params = {per_page: batch_size(options)}.merge(params)
75
-
76
- launch(
77
- data,
78
- method: :get,
79
- params: params,
80
- silent: silent,
81
- options: options
82
- ).tap do |status|
83
- status.mode = :search
84
-
85
- entries = status.queue
86
- puts "\n"
87
- entries.each_with_index do |entry, i|
88
- if (i % 10).zero?
89
- percent = i * 100 / entries.length
90
- print "Searching: #{percent.round}% (#{i}/#{entries.length} entries)\r"
91
- $stdout.flush
92
- end
93
-
94
- next if status.success?(entry)
95
-
96
- email = nil
97
- if entry.respond_to?(:email)
98
- email = entry.email
99
- elsif entry.respond_to?(:to_h)
100
- email = entry.to_h["email"]
101
- end
102
-
103
- people_matching = []
104
- email = email.to_s.strip.downcase
105
- unless email.empty?
106
- people_matching = get(
107
- params: params.merge(q: email),
108
- silent: silent,
109
- options: options
110
- ).select do |person|
111
- person.email == email
112
- end
113
- end
114
-
115
- case people_matching.length
116
- when 1
117
- status.set_person_match(entry, people_matching.first)
118
- when 2..Float::INFINITY
119
- status.set_people_match(entry, people_matching)
120
- end
121
- end
122
- end
123
- end
124
-
125
- private
126
-
127
- def get(params: {}, silent: false, options: self.options)
128
- msg = "cannot batch get without api connnection, please provide a valid api connection!"
129
- fatal msg unless (people_api = api&.people)
130
-
131
- params = {per_page: batch_size(options)}.merge(params)
132
- people_api.get_all(params: params, silent: silent)
133
- end
134
-
135
- def batch_from(
136
- data,
137
- method:,
138
- params: {},
139
- silent: false,
140
- options: self.options
141
- )
142
- fatal "Invalid batch method: #{method}." unless self.class.valid_method?(method)
143
- return nil if !data || !data.is_a?(Enumerable)
144
-
145
- msg = "cannot batch #{method} without api connnection, please provide a valid api connection!"
146
- fatal msg unless (people_api = api&.people)
147
-
148
- # param q does not make sense here, even for GET method
149
- params = {per_page: batch_size(options)}.merge(params)
150
- per_page = params[:per_page] || batch_size(options)
151
-
152
- launch_batch(
153
- data,
154
- method: method,
155
- per_page: per_page,
156
- people_api: people_api,
157
- silent: silent,
158
- options: options
159
- )
160
- end
161
-
162
- # Default way to retrieve options (unless provided)
163
- def options
164
- ASSETS.cli.options
165
- end
166
-
167
- def launch_batch( # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
168
- data,
169
- method:,
170
- status: nil,
171
- job_mode: true, # rubocop:disable Lint/UnusedMethodArgument
172
- options: self.options,
173
- per_page: batch_size(options),
174
- people_api: api&.people,
175
- silent: false
176
- )
177
- iteration = 1
178
- done = 0
179
- iterations = (data.length.to_f / per_page).ceil
180
-
181
- status ||= Eco::API::Session::Batch::Status.new(
182
- enviro,
183
- queue: data,
184
- method: method
185
- )
186
-
187
- status.tap do
188
- pending_for_server_error = data.to_a[0..]
189
-
190
- start_time = Time.now
191
-
192
- data.each_slice(per_page) do |slice|
193
- msg = "starting batch '#{method}' iteration #{iteration}/#{iterations}, "
194
- msg << "with #{slice.length} entries of #{data.length} -- #{done} done"
195
- msg << (" " * 20)
196
- log(:info) { msg } unless silent
197
-
198
- start_slice = Time.now
199
-
200
- offer_retry_on(Ecoportal::API::Errors::TimeOut) do
201
- people_api.batch(job_mode: job_mode?(options)) do |batch|
202
- slice.each do |person|
203
- batch.public_send(method, person) do |response|
204
- faltal("Request with no response") unless response
205
-
206
- next if server_error?(response)
207
-
208
- pending_for_server_error.delete(person)
209
- status[person] = response
210
- end
211
- end
212
- end # end batch
213
- end
214
-
215
- done += slice.length
216
-
217
- msg = " ... iteration #{iteration}/#{iterations} done "
218
- msg << "in #{str_stats(start_slice, slice.length)} "
219
- msg << "(average: #{str_stats(start_time, done)})"
220
- msg << (" " * 20)
221
- log(:info) { msg } unless silent
222
-
223
- iteration += 1
224
- end # next slice
225
-
226
- # temporary working around (due to back-end problems with batch/jobs)
227
- unless pending_for_server_error.empty?
228
- msg = "Going to re-try #{pending_for_server_error.count} due to server errors"
229
- log(:info) { msg } unless silent
230
-
231
- launch_batch(
232
- pending_for_server_error,
233
- status: status,
234
- method: method,
235
- job_mode: false,
236
- per_page: per_page,
237
- people_api: people_api,
238
- silent: silent,
239
- options: options
240
- )
241
- end
242
- end
243
- end
244
-
245
- def server_error?(response)
246
- res_status = response.status
247
- server_error = !res_status || res_status.server_error?
248
- other_error = !server_error && (!res_status.code || res_status.code < 100)
249
- no_body = !server_error && !other_error && !response.body
250
- server_error || other_error || no_body
251
- end
252
-
253
- def offer_retry_on(error_type, retries_left = 3, &block)
254
- yield
255
- rescue error_type => err
256
- raise err.class, err.message, cause: nil unless retries_left.positive?
257
-
258
- explanation = "#{err}\n"
259
- explanation << "You have #{retries_left} retries left."
260
- question = " Do you want to retry (y/N)?"
261
-
262
- prompt_user(question, default: "Y", explanation: explanation, timeout: 10) do |response|
263
- raise unless response.upcase.start_with?("Y")
264
-
265
- puts "\nOkay... let's retry!"
266
- offer_retry_on(error_type, retries_left - 1, &block)
267
- end
268
- end
269
-
270
- def str_stats(start, count)
271
- now = Time.now
272
- secs = (now - start).round(2)
273
- if secs > 0.0
274
- per_sec = (count.to_f / secs).round(2)
275
- "#{secs}s -> #{per_sec} people/s"
276
- else
277
- " -- "
278
- end
20
+ def launch(...)
21
+ batch_from(...)
279
22
  end
280
23
  end
281
24
  end
data/lib/eco/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Eco
2
- VERSION = '3.0.16'.freeze
2
+ VERSION = '3.0.17'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eco-helpers
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.16
4
+ version: 3.0.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oscar Segura
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-14 00:00:00.000000000 Z
11
+ date: 2024-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -229,7 +229,7 @@ dependencies:
229
229
  version: '0.10'
230
230
  - - ">="
231
231
  - !ruby/object:Gem::Version
232
- version: 0.10.4
232
+ version: 0.10.5
233
233
  type: :runtime
234
234
  prerelease: false
235
235
  version_requirements: !ruby/object:Gem::Requirement
@@ -239,7 +239,7 @@ dependencies:
239
239
  version: '0.10'
240
240
  - - ">="
241
241
  - !ruby/object:Gem::Version
242
- version: 0.10.4
242
+ version: 0.10.5
243
243
  - !ruby/object:Gem::Dependency
244
244
  name: ecoportal-api-graphql
245
245
  requirement: !ruby/object:Gem::Requirement
@@ -656,8 +656,17 @@ files:
656
656
  - lib/eco/api/session/batch/job.rb
657
657
  - lib/eco/api/session/batch/jobs.rb
658
658
  - lib/eco/api/session/batch/jobs_groups.rb
659
+ - lib/eco/api/session/batch/launcher.rb
660
+ - lib/eco/api/session/batch/launcher/benchmarking.rb
661
+ - lib/eco/api/session/batch/launcher/mode.rb
662
+ - lib/eco/api/session/batch/launcher/options.rb
663
+ - lib/eco/api/session/batch/launcher/retry.rb
664
+ - lib/eco/api/session/batch/launcher/size.rb
665
+ - lib/eco/api/session/batch/launcher/status_handling.rb
666
+ - lib/eco/api/session/batch/launcher/valid_methods.rb
659
667
  - lib/eco/api/session/batch/policies.rb
660
668
  - lib/eco/api/session/batch/request_stats.rb
669
+ - lib/eco/api/session/batch/searcher.rb
661
670
  - lib/eco/api/session/batch/status.rb
662
671
  - lib/eco/api/session/config.rb
663
672
  - lib/eco/api/session/config/api.rb