eco-helpers 3.0.16 → 3.0.17

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 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