eco-helpers 3.2.12 → 3.2.13

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: 91691c5a914be5eebeff46f776d9c5b720e8e9672317051f36b5995beff3977c
4
- data.tar.gz: 1742764775b28b99f136e451cc79c68359924f7722752ccdae043cf7c1b7f6a9
3
+ metadata.gz: 164a0d7e0bea8396208dae904e45bac439f288773a138e01869fe19ec9eafe21
4
+ data.tar.gz: b56add1010ab7feee79f58c8bac0835bc39f9bfceb8a56fcf1cb6aff176bdd1d
5
5
  SHA512:
6
- metadata.gz: 3c17e149406ae8ae64c94b623e10a6151d9f0fd4755e736281a89c098c3c6ff180d7f85560d62e63741f3166d283882352e50cf48841c22e4f2106a17e76f3f0
7
- data.tar.gz: 25199fc81a46ff8897be9af805bb675a4ee3ca11b61439a91450639703583c1907e97513fd26e14375a30956166dcb2cb35161a56b920f221ff6d2efc69bcee4
6
+ metadata.gz: 9a584c9593325bbd6fa7ec08794de3d839bd675c719613899c25e7d3d53bbfb58b03bc33f703b0b38a906a0ce08c30ec31c11d180653e9181f25b510db3bc3dd
7
+ data.tar.gz: ddfd53f1a2e72cbbf4136ee4ebb25481e098512ac258ff21bfea188417a6824e22aab407f16cb3a3d966b21d320b6aa56659e71f0782accda71f921d5db24601
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.2.13] - 2026-01-xx
5
+ ## [3.2.14] - 2026-04-xx
6
6
 
7
7
  ### Added
8
8
 
@@ -10,6 +10,23 @@ All notable changes to this project will be documented in this file.
10
10
 
11
11
  ### Fixed
12
12
 
13
+ ## [3.2.13] - 2026-04-15
14
+
15
+ ### Added
16
+
17
+ - `-split-csv` case
18
+ - Allow custom split criteria via `splitter` named argument.
19
+ - `-merge-csv` case
20
+
21
+ ### Changed
22
+
23
+ - improved `Stream` with methods `eof?` and `shift`
24
+
25
+ ### Fixed
26
+
27
+ - Locations remap on RS update
28
+ - `-group-csv`: correct rows count
29
+
13
30
  ## [3.2.12] - 2026-01-19
14
31
 
15
32
  ### Added
@@ -0,0 +1,27 @@
1
+ class Eco::API::UseCases::Default::Utils::MergeCsv
2
+ class Cli < Eco::API::UseCases::Cli
3
+ str_desc = 'Merges the csv rows by a pivot field. '
4
+ str_desc << 'It assumes the pivot field is sorted '
5
+ str_desc << '(same values should be consecutive)'
6
+
7
+ desc str_desc
8
+
9
+ callback do |_session, options, _usecase|
10
+ if (file = SCR.get_file(cli_name, required: true, should_exist: true))
11
+ options.deep_merge!(input: {file: {name: file}})
12
+ end
13
+ end
14
+
15
+ add_option('-merge', 'The CSV file that should be merged onto the original') do |options|
16
+ if (file = SCR.get_file('-merge', required: true, should_exist: true))
17
+ options.deep_merge!(input: {merge_file: {name: file}})
18
+ end
19
+ end
20
+
21
+ add_option('-by', 'The column that should be used to merge') do |options|
22
+ if (file = SCR.get_arg('-by', with_param: true))
23
+ options.deep_merge!(input: {merge_by_field: file})
24
+ end
25
+ end
26
+ end
27
+ end
@@ -27,7 +27,6 @@ class Eco::API::UseCases::Default::Utils::GroupCsv < Eco::API::Custom::UseCase
27
27
  private
28
28
 
29
29
  def generate_file # rubocop:disable Metrics/AbcSize
30
- row_count = 0
31
30
  in_index = nil
32
31
 
33
32
  CSV.open(output_filename, 'wb') do |out_csv|
@@ -49,24 +48,19 @@ class Eco::API::UseCases::Default::Utils::GroupCsv < Eco::API::Custom::UseCase
49
48
  next unless pivotable?(row, idx)
50
49
  next unless (last_group = pivot_row(row))
51
50
 
52
- row_count += 1
53
-
54
- if (row_count % 500).zero?
55
- print "... Done #{row_count} rows \r"
56
- $stdout.flush
57
- end
51
+ row_count!
58
52
 
59
53
  out_csv << last_group.values_at(*headers)
60
54
  end
61
55
 
62
56
  # finalize
63
- if (lrow = pivot_row)
64
- row_count += 1
65
- out_csv << lrow.values_at(*headers)
57
+ if (l_row = pivot_row)
58
+ row_count!
59
+ out_csv << l_row.values_at(*headers)
66
60
  end
67
61
  ensure
68
62
  msg = "Generated file '#{output_filename}' "
69
- msg << "with #{row_count} rows (out of #{in_index})."
63
+ msg << "with #{row_count} rows (out of #{in_index + 1})."
70
64
 
71
65
  log(:info) { msg } unless simulate?
72
66
  end
@@ -97,7 +91,7 @@ class Eco::API::UseCases::Default::Utils::GroupCsv < Eco::API::Custom::UseCase
97
91
  last unless last == @group
98
92
  end
99
93
 
100
- attr_reader :group
94
+ attr_reader :group, :row_count
101
95
  attr_reader :headers, :headers_rest
102
96
 
103
97
  def headers!(row)
@@ -112,11 +106,21 @@ class Eco::API::UseCases::Default::Utils::GroupCsv < Eco::API::Custom::UseCase
112
106
  instance_variable_defined?(:@headers)
113
107
  end
114
108
 
109
+ def row_count!
110
+ @row_count ||= 0
111
+ (@row_count += 1).tap do |cnt|
112
+ if (cnt % 500).zero?
113
+ print "... Done #{cnt} rows \r"
114
+ $stdout.flush
115
+ end
116
+ end
117
+ end
118
+
115
119
  def pivotable?(row, idx)
116
120
  return true unless row[group_by_field].to_s.strip.empty?
117
121
 
118
122
  msg = "Row #{idx} doesn't have value for pivot field '#{group_by_field}'"
119
- msg << '. Skipping (discared) ...'
123
+ msg << '. Skipping (discarded) ...'
120
124
  log(:warn) { msg }
121
125
  false
122
126
  end
@@ -130,7 +134,7 @@ class Eco::API::UseCases::Default::Utils::GroupCsv < Eco::API::Custom::UseCase
130
134
  end
131
135
 
132
136
  def start_at
133
- return nil unless (num = options.dig(:input, :file, :start_at))
137
+ return unless (num = options.dig(:input, :file, :start_at))
134
138
 
135
139
  num = num.to_i
136
140
  num = nil if num.zero?
@@ -138,7 +142,7 @@ class Eco::API::UseCases::Default::Utils::GroupCsv < Eco::API::Custom::UseCase
138
142
  end
139
143
 
140
144
  def output_filename
141
- return nil unless input_name
145
+ return unless input_name
142
146
 
143
147
  File.join(input_dir, "#{input_name}_grouped#{input_ext}")
144
148
  end
@@ -0,0 +1,313 @@
1
+ # This script assumes that for the `MERGE_BY_FIELD` rows are consecutive.
2
+ # @note you might run first the `sort-csv` case.
3
+ # @note at the moment, it does NOT add new fields from the merge file.
4
+ # It only uses the headers of the original file.
5
+ # @note you must inherit from this case and define the constants.
6
+ #
7
+ # MERGE_BY_FIELD = 'target_csv_field'.freeze
8
+ # # those not merged are overridden
9
+ # JOINED_FIELDS = [
10
+ # 'joined_field_1',
11
+ # 'joined_field_2',
12
+ # 'joined_field_3',
13
+ # ].freeze
14
+ #
15
+ class Eco::API::UseCases::Default::Utils::MergeCsv < Eco::API::Custom::UseCase
16
+ name 'merge-csv'
17
+ type :other
18
+
19
+ require_relative 'cli/merge_csv_cli'
20
+
21
+ def main(*_args)
22
+ if simulate?
23
+ count = Eco::CSV.count(input_file)
24
+ log(:info) { "CSV '#{input_file}' has #{count} rows." }
25
+ else
26
+ generate_file
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def generate_file # rubocop:disable Metrics/AbcSize
33
+ in_index = nil
34
+
35
+ CSV.open(output_filename, 'wb') do |out_csv|
36
+ pending = false
37
+ first = true
38
+ m_first = true
39
+ row = nil
40
+ idx = nil
41
+
42
+ puts "\n"
43
+
44
+ streamed_merging.for_each do |m_row, m_idx|
45
+ if m_first
46
+ m_first = false
47
+ require_merge_by_field!(m_row, file: merge_file)
48
+ end
49
+
50
+ next unless pivotable?(m_row, m_idx, file: merge_file)
51
+
52
+ merging_row(m_row)
53
+ merge_done = false
54
+
55
+ loop do
56
+ unless pending
57
+ row = nil
58
+ streamed_input.shift do |o_row, i|
59
+ idx = i
60
+ row = o_row
61
+
62
+ if first
63
+ first = false
64
+ headers!(row)
65
+ out_csv << headers
66
+ require_merge_by_field!(row, file: input_file)
67
+ end
68
+ end
69
+ end
70
+
71
+ break unless row
72
+
73
+ in_index = idx
74
+ next unless pivotable?(row, idx, file: input_file)
75
+
76
+ row_count!
77
+ added = original_row(row) do |merged_row, merged:|
78
+ out_csv << merged_row.values_at(*headers)
79
+ merge_done = true if merged
80
+ end
81
+
82
+ pending = !added
83
+
84
+ break if merge_done
85
+ break unless added
86
+ break if streamed_input.eof?
87
+ end
88
+
89
+ row = nil unless pending
90
+
91
+ if pending || streamed_input.eof?
92
+ msg = "Could not merge row #{m_idx} (#{merging_row[merge_by_field]}) "
93
+ msg << "because the pivot value does not exist in the original file"
94
+ msg << ". Skipping (discarded) ..."
95
+ log(:warn) { msg }
96
+ end
97
+ end
98
+
99
+ # finalize
100
+ loop do
101
+ row = nil
102
+ streamed_input.shift do |o_row, i|
103
+ idx = i
104
+ row = o_row
105
+ end
106
+
107
+ break unless row
108
+
109
+ in_index = idx
110
+ next unless pivotable?(row, idx, file: input_file)
111
+
112
+ row_count!
113
+ out_csv << row.values_at(*headers)
114
+
115
+ break if streamed_input.eof?
116
+ end
117
+ ensure
118
+ msg = "Generated file '#{output_filename}' "
119
+ msg << "with #{row_count} rows (out of #{in_index + 1})."
120
+
121
+ log(:info) { msg } unless simulate?
122
+ end
123
+ end
124
+
125
+ # It tracks the current merging row
126
+ # @return [Nil, Hash] the last merge row when `row` doesn't belong
127
+ # or `nil` otherwise
128
+ def merging_row(row = nil)
129
+ return @merging_row unless row
130
+
131
+ @merging_row = row.to_h
132
+ end
133
+
134
+ # It tracks the current grouped row
135
+ # @return [Nil, Hash] the last grouped row when `row` doesn't belong
136
+ # or `nil` otherwise
137
+ def original_row(row)
138
+ pivot_value = row[merge_by_field]
139
+ merge_pivot = merging_row[merge_by_field]
140
+
141
+ if pivot_value > merge_pivot
142
+ # as both files are sorted, we can't add the original row now
143
+ # and we need to just return false
144
+ return false
145
+ elsif pivot_value < merge_pivot
146
+ yield(row.to_h, merged: false) if block_given?
147
+ return true
148
+ end
149
+
150
+ merged_row = {}
151
+ merged_row = {merge_by_field => pivot_value}
152
+
153
+ joined_fields.each do |field|
154
+ original_values = row[field].to_s.split('|').compact.uniq
155
+ merge_values = merging_row[field].to_s.split('|').compact.uniq
156
+
157
+ merged_row[field] = (original_values | merge_values).join('|')
158
+ merged_row[field] = nil if merged_row[field].to_s.strip.empty?
159
+ end
160
+
161
+ headers_rest.each do |field|
162
+ merged_row[field] = row[field]
163
+ merged_row[field] = merging_row[field] if merging_row.key?(field)
164
+ merged_row[field] = nil if merged_row[field].to_s.strip.empty?
165
+ end
166
+
167
+ missed_headers = (merging_row.keys - headers)
168
+ if missed_headers.any? && !warned_missed_headers?
169
+ msg = "Missing headers in merged file: #{missed_headers.join(', ')}"
170
+ log(:warn) { msg }
171
+ @warned_missed_headers = true
172
+ end
173
+
174
+ merged_row = merged_row.slice(*headers)
175
+ yield(merged_row, merged: true) if block_given?
176
+
177
+ true
178
+ end
179
+
180
+ attr_reader :merge, :row_count
181
+ attr_reader :headers, :headers_rest
182
+
183
+
184
+ # Whether if we already warned about merging headers that
185
+ # are not in the original
186
+ def warned_missed_headers?
187
+ @warned_missed_headers ||= false
188
+ end
189
+
190
+ def headers!(row)
191
+ return if headers?
192
+
193
+ @headers = row.to_h.keys
194
+ @joined_fields = @headers & joined_fields
195
+ @headers_rest = @headers - @joined_fields - [merge_by_field]
196
+ @headers = [merge_by_field, *@joined_fields, *@headers_rest]
197
+ end
198
+
199
+ def headers?
200
+ instance_variable_defined?(:@headers)
201
+ end
202
+
203
+ def row_count!
204
+ @row_count ||= 0
205
+ (@row_count += 1).tap do |cnt|
206
+ if (cnt % 500).zero?
207
+ print "... Done #{cnt} rows \r"
208
+ $stdout.flush
209
+ end
210
+ end
211
+ end
212
+
213
+ def pivotable?(row, idx, file:)
214
+ return false if row.nil?
215
+ return true unless row[merge_by_field].to_s.strip.empty?
216
+
217
+ msg = "Row #{idx} doesn't have value for pivot field '#{merge_by_field}'"
218
+ msg << " (file: '#{file}'). Skipping (discarded) ..."
219
+ log(:warn) { msg }
220
+ false
221
+ end
222
+
223
+ def streamed_input
224
+ @streamed_input ||= Eco::CSV::Stream.new(input_file)
225
+ end
226
+
227
+ def streamed_merging
228
+ @streamed_merging ||= Eco::CSV::Stream.new(merge_file)
229
+ end
230
+
231
+ def input_file
232
+ options.dig(:input, :file, :name)
233
+ end
234
+
235
+ def merge_file
236
+ options.dig(:input, :merge_file, :name)
237
+ end
238
+
239
+ def output_filename
240
+ return unless input_name
241
+
242
+ File.join(input_dir, "#{input_name}_merged#{input_ext}")
243
+ end
244
+
245
+ def input_name
246
+ @input_name ||= File.basename(input_basename, input_ext)
247
+ end
248
+
249
+ def input_ext
250
+ @input_ext ||= input_basename.split('.')[1..].join('.').then do |name|
251
+ ".#{name}"
252
+ end
253
+ end
254
+
255
+ def input_basename
256
+ @input_basename ||= File.basename(input_full_filename)
257
+ end
258
+
259
+ def input_dir
260
+ @input_dir = File.dirname(input_full_filename)
261
+ end
262
+
263
+ def input_full_filename
264
+ @input_full_filename ||= File.expand_path(input_file)
265
+ end
266
+
267
+ def require_merge_by_field!(row, file:)
268
+ return true if row.key?(merge_by_field)
269
+
270
+ msg = "Pivot field '#{merge_by_field}' missing in header of file '#{file}'"
271
+ log(:error) { msg }
272
+ raise msg
273
+ end
274
+
275
+ def merge_by_field
276
+ return @merge_by_field if instance_variable_defined?(:@merge_by_field)
277
+
278
+ return (@merge_by_field = opts_merge_by) if opts_merge_by
279
+
280
+ unless self.class.const_defined?(:MERGE_BY_FIELD)
281
+ msg = "(#{self.class}) You must define MERGE_BY_FIELD constant"
282
+ log(:error) { msg }
283
+ raise msg
284
+ end
285
+
286
+ @merge_by_field = self.class::MERGE_BY_FIELD
287
+ end
288
+
289
+ def joined_fields
290
+ return @joined_fields if instance_variable_defined?(:@joined_fields)
291
+
292
+ unless self.class.const_defined?(:JOINED_FIELDS)
293
+ msg = "(#{self.class}) You must define JOINED_FIELDS constant"
294
+ log(:error) { msg }
295
+ raise msg
296
+ end
297
+
298
+ @joined_fields ||= [self.class::JOINED_FIELDS].flatten.compact.tap do |flds|
299
+ next unless flds.empty?
300
+
301
+ log(:warn) {
302
+ msg = 'There were no fields to be joined (JOINED_FIELDS). '
303
+ msg << 'This means all fields present in the merging file '
304
+ msg << ' will be overridden in the original file.'
305
+ msg
306
+ }
307
+ end
308
+ end
309
+
310
+ def opts_merge_by
311
+ options.dig(:input, :merge_by_field)
312
+ end
313
+ end
@@ -1,7 +1,7 @@
1
1
  class Eco::API::UseCases::Default::Utils::SplitCsv < Eco::API::Common::Loaders::UseCase
2
2
  require_relative 'cli/split_csv_cli'
3
3
 
4
- MAX_ROWS = 15_000
4
+ MAX_ROWS = :unused
5
5
 
6
6
  name 'split-csv'
7
7
  type :other
@@ -15,6 +15,7 @@ class Eco::API::UseCases::Default::Utils::SplitCsv < Eco::API::Common::Loaders::
15
15
  input_file,
16
16
  max_rows: max_rows,
17
17
  start_at: start_at,
18
+ **params,
18
19
  &filter
19
20
  ).tap do |split|
20
21
  msg = []
@@ -31,6 +32,10 @@ class Eco::API::UseCases::Default::Utils::SplitCsv < Eco::API::Common::Loaders::
31
32
 
32
33
  private
33
34
 
35
+ def params
36
+ {}
37
+ end
38
+
34
39
  def filter
35
40
  nil
36
41
  end
@@ -14,4 +14,5 @@ require_relative 'utils/split_json_case'
14
14
  require_relative 'utils/json_to_csv_case'
15
15
  require_relative 'utils/sort_csv_case'
16
16
  require_relative 'utils/group_csv_case'
17
+ require_relative 'utils/merge_csv_case'
17
18
  require_relative 'utils/entries_to_csv_case'
@@ -40,7 +40,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location::Command
40
40
  return nil unless error?
41
41
 
42
42
  msg = []
43
- msg << "(#{command} '#{node_id}') #{error.message}"
43
+ msg << "(#{command_type} '#{node_id}') #{error.message}"
44
44
 
45
45
  feed = []
46
46
  feed.concat(error.validationErrors.map(&:message)) unless error.validationErrors.empty?
@@ -55,7 +55,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location::Command
55
55
  end
56
56
 
57
57
  def command_input_data
58
- input[command]
58
+ input[command_type]
59
59
  end
60
60
 
61
61
  def command_id
@@ -53,7 +53,8 @@ module Eco::API::UseCases::GraphQL::Helpers::Location::Command
53
53
  next applied unless with_id_change
54
54
 
55
55
  applied.select do |result|
56
- next false unless (command = result.command_result_data)
56
+ # next false unless (command = result.command_result_data)
57
+ next false unless (command = result.command_input_data)
57
58
 
58
59
  command.keys.include?(:newId)
59
60
  end
@@ -22,9 +22,10 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
22
22
  # both are being moved (specific/long mappings first)
23
23
  return 1 if from.subset_of?(other.from)
24
24
  return -1 if from.superset_of?(other.from)
25
- return -1 if (from & other.from).empty?
25
+ return -1 unless from.intersect?(other.from)
26
26
  return -1 if from.length >= other.from.length
27
27
  return 1 if from.length < other.from.length
28
+
28
29
  -1
29
30
  end
30
31
 
@@ -49,16 +50,19 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
49
50
  def maps?
50
51
  return false if any?(&:empty?)
51
52
  return false if from == to
53
+
52
54
  true
53
55
  end
54
56
 
55
57
  def rename?
56
58
  return false unless maps?
59
+
57
60
  both? {|set| set.length == 1}
58
61
  end
59
62
 
60
63
  def move?
61
64
  return false unless maps?
65
+
62
66
  !rename?
63
67
  end
64
68
  end
@@ -4,7 +4,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
4
4
  class << self
5
5
  def attr_compare(*attrs)
6
6
  attrs.each do |attr|
7
- meth = "#{attr}".to_sym # rubocop:disable Style/RedundantInterpolation
7
+ meth = :"#{attr}"
8
8
  define_method meth do |value|
9
9
  set.send(meth, to_set(value))
10
10
  end
@@ -13,7 +13,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
13
13
 
14
14
  def attr_operate(*attrs)
15
15
  attrs.each do |attr|
16
- meth = "#{attr}".to_sym # rubocop:disable Style/RedundantInterpolation
16
+ meth = :"#{attr}"
17
17
  define_method meth do |value|
18
18
  self.class.new(set.send(meth, to_set(value)))
19
19
  end
@@ -57,6 +57,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
57
57
  def include?(value)
58
58
  value = value.to_s.strip
59
59
  return false if value.empty?
60
+
60
61
  set.include?(value)
61
62
  end
62
63
 
@@ -82,7 +83,9 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
82
83
  return value.ini_tags.dup if value.is_a?(self.class)
83
84
  return value.dup if value.is_a?(Array)
84
85
  return value.to_a if value.is_a?(Set)
85
- raise ArgumentError, "Expecting #{self.class}, Set or Array. Given: #{value.class}"
86
+
87
+ msg = "Expecting #{self.class}, Set or Array. Given: #{value.class}"
88
+ raise ArgumentError, msg
86
89
  end
87
90
 
88
91
  def to_set(value)
@@ -22,7 +22,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
22
22
  end
23
23
 
24
24
  def to_csv(filename)
25
- CSV.open(filename, "w") do |fd|
25
+ CSV.open(filename, 'w') do |fd|
26
26
  fd << %w[src_tags dst_tags]
27
27
 
28
28
  each do |tags_map|
@@ -67,7 +67,8 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
67
67
  end
68
68
 
69
69
  def <<(pair)
70
- raise ArgumentError, "Expecting pair of Array in Array. Given: #{pair}" unless self.class.correct_pair?(pair)
70
+ msg = "Expecting pair of Array in Array. Given: #{pair}"
71
+ raise ArgumentError, msg unless self.class.correct_pair?(pair)
71
72
 
72
73
  add(*pair)
73
74
  end
@@ -76,6 +76,8 @@ class Eco::API::UseCases::GraphQL::Samples::Location
76
76
  ) do |input, stage|
77
77
  next unless input
78
78
 
79
+ self.id_name_input = input if simulate? && stage == :id_name
80
+
79
81
  some_update = true
80
82
 
81
83
  sliced_batches(
@@ -98,8 +100,8 @@ class Eco::API::UseCases::GraphQL::Samples::Location
98
100
  rearchive
99
101
  end
100
102
 
101
- rescued { delete_or_publish_draft }
102
- rescued { manage_remaps_table }
103
+ rescued { delete_or_publish_draft }
104
+ rescued { manage_remaps_table if some_update }
103
105
  end
104
106
  end
105
107
 
@@ -131,6 +133,8 @@ class Eco::API::UseCases::GraphQL::Samples::Location
131
133
 
132
134
  private
133
135
 
136
+ attr_accessor :id_name_input
137
+
134
138
  # Work with adapted diff builders.
135
139
  def nodes_diff_class
136
140
  Eco::API::UseCases::GraphQL::Helpers::Location::Command::Diffs
@@ -231,11 +235,17 @@ class Eco::API::UseCases::GraphQL::Samples::Location
231
235
  end
232
236
 
233
237
  def manage_remaps_table
234
- return unless results.final_response?
235
-
236
238
  rescued do
237
- results.applied_commands(with_id_change: true) do |result|
238
- update_tags_remap_table(result.command)
239
+ if simulate? && id_name_input
240
+ id_name_input[:commands].each do |command|
241
+ update_tags_remap_table(command[:update])
242
+ end
243
+ elsif results.final_response?
244
+ results.applied_commands(with_id_change: true).each do |result|
245
+ update_tags_remap_table(result.command_input_data)
246
+ end
247
+ else
248
+ return
239
249
  end
240
250
  end
241
251
 
@@ -36,8 +36,9 @@ class Eco::API::UseCases::GraphQL::Samples::Location
36
36
  # @note the SFTP push only happens if `remote_subfolder` is defined, via:
37
37
  # 1. `options.dig(:sftp, :remote_subfolder)`
38
38
  # 2. `REMOTE_FOLDER` const
39
- def close_handling_tags_remap_csv
39
+ def close_handling_tags_remap_csv # rubocop:disable Naming/PredicateMethod
40
40
  return false unless super
41
+ return true if simulate?
41
42
 
42
43
  upload(tags_remap_csv_file) unless remote_subfolder.nil?
43
44
  true
data/lib/eco/csv/split.rb CHANGED
@@ -3,14 +3,17 @@ module Eco
3
3
  class Split
4
4
  include Eco::Language::AuxiliarLogger
5
5
 
6
+ MAX_ROWS_DEFAULT = 1_000_000
7
+
6
8
  attr_reader :filename
7
9
 
8
- def initialize(filename, max_rows:, start_at: nil, **kargs)
10
+ def initialize(filename, max_rows: :unused, start_at: nil, **kargs)
9
11
  msg = "File '#{filename}' does not exist"
10
12
  raise ArgumentError, msg unless ::File.exist?(filename)
11
13
 
12
14
  @filename = filename
13
15
  @max_rows = max_rows
16
+ @max_rows = MAX_ROWS_DEFAULT if max_rows == :unused
14
17
  @start_at = start_at
15
18
  @params = kargs
16
19
 
@@ -34,16 +37,17 @@ module Eco
34
37
  @out_files ||= []
35
38
  end
36
39
 
37
- # @yield [idx, file] a block to spot the filename
40
+ # @yield [row, ridx, fidx, file] block to spot if the row should be included
38
41
  # @yieldparam idx [Integer] the number of the file
39
42
  # @yieldparam file [String] the default name of the file
40
- # @yieldreturn [String] the filename of the file `idx`.
41
- # - If `nil` it will create its own filename convention
43
+ # @yieldparam fidx [Integer] the number of the file
44
+ # @yieldparam file [String] the default name of the file
45
+ # @yieldreturn [Bollean] whether the row should be included
42
46
  # @return [Array<String>] names of the generated files
43
- def call(&block)
47
+ def call(&filter)
44
48
  stream.for_each(start_at_idx: start_at) do |row, ridx|
45
49
  self.total_count += 1
46
- copy_row(row, ridx, &block)
50
+ copy_row(row, ridx, &filter)
47
51
  end
48
52
 
49
53
  out_files
@@ -56,33 +60,42 @@ module Eco
56
60
 
57
61
  attr_reader :params
58
62
  attr_reader :idx, :max_rows, :start_at
59
- attr_reader :headers, :row_idx
63
+ attr_reader :headers, :row_idx, :out_row_idx
64
+ attr_reader :last_cut_desc
60
65
 
61
66
  attr_accessor :exception
62
67
 
63
- def copy_row(row, ridx, &block)
68
+ def copy_row(row, ridx)
64
69
  @headers ||= row.headers
65
70
  @row_idx = ridx
66
71
 
67
- current_csv(ridx) do |csv, fidx, file_out|
72
+ current_csv(row) do |csv, fidx, file_out|
68
73
  included = true
69
- included &&= !block || yield(row, ridx, fidx, file_out)
74
+ included &&= yield(row, ridx, fidx, file_out) if block_given?
70
75
  next unless included
71
76
 
77
+ @out_row_idx += 1
72
78
  self.copy_count += 1
73
79
  csv << row.fields
74
80
  end
75
81
  end
76
82
 
77
- def current_csv(ridx)
78
- if split?(ridx) || @csv.nil?
79
- puts "Split at row #{row_idx}"
83
+ def current_csv(row)
84
+ if (cut = split?(row, &splitter)) || @csv.nil?
85
+ cut = nil if cut.is_a?(TrueClass) || cut.to_s.empty? || !cut
86
+ msg = "Split at row #{row_idx}"
87
+ msg << " (cut: #{cut})" unless cut.nil?
88
+ puts msg
89
+
90
+ @last_cut_desc = cut unless cut.nil?
91
+
80
92
  @csv&.close
81
93
 
82
- out_filename = generate_name(next_idx)
94
+ out_filename = generate_name(next_idx, desc: last_cut_desc)
83
95
  @csv = ::CSV.open(out_filename, "w")
84
96
  @csv << headers
85
97
  out_files << out_filename
98
+ @out_row_idx = 0
86
99
  end
87
100
 
88
101
  yield(@csv, idx, out_files.last) if block_given?
@@ -90,8 +103,19 @@ module Eco
90
103
  @csv
91
104
  end
92
105
 
93
- def split?(ridx)
94
- ((ridx + 1) % max_rows).zero?
106
+ # @note client scripts can tweak this method.
107
+ def split?(row)
108
+ return yield(row, row_idx) if block_given?
109
+
110
+ ((row_idx + 1) % max_rows).zero?
111
+ end
112
+
113
+ def splitter
114
+ @splitter ||= params[:splitter]
115
+ end
116
+
117
+ def splitter?
118
+ splitter.is_a?(Proc)
95
119
  end
96
120
 
97
121
  def next_idx
@@ -103,11 +127,15 @@ module Eco
103
127
  end
104
128
 
105
129
  def stream
106
- @stream ||= Eco::CSV::Stream.new(filename, **params)
130
+ @stream ||= Eco::CSV::Stream.new(
131
+ filename,
132
+ **params
133
+ )
107
134
  end
108
135
 
109
- def generate_name(fidx)
110
- File.join(input_dir, "#{input_name}_#{file_number(fidx)}#{input_ext}")
136
+ def generate_name(fidx, desc: nil)
137
+ desc = "_#{desc}" unless desc.nil?
138
+ File.join(input_dir, "#{input_name}_#{file_number(fidx)}#{desc}#{input_ext}")
111
139
  end
112
140
 
113
141
  def file_number(num)
@@ -3,6 +3,16 @@ module Eco
3
3
  class Stream
4
4
  include Eco::Language::AuxiliarLogger
5
5
 
6
+ CSV_PARAMS = %i[
7
+ col_sep row_sep quote_char
8
+ headers skip_blanks skip_lines
9
+ nil_value empty_value
10
+ converters unconverted_fields
11
+ return_headers header_converters
12
+ liberal_parsing
13
+ field_size_limit
14
+ ].freeze
15
+
6
16
  attr_reader :filename
7
17
 
8
18
  def initialize(filename, **kargs)
@@ -16,9 +26,42 @@ module Eco
16
26
  init
17
27
  end
18
28
 
29
+ def eof?
30
+ started? && !row
31
+ end
32
+
33
+ def started?
34
+ @started ||= false
35
+ end
36
+
37
+ def shift
38
+ raise ArgumentError, 'Expecting block, but not given.' unless block_given?
39
+
40
+ @started = true
41
+ yield(row, next_idx) if (self.row = csv.shift)
42
+ rescue StandardError => err
43
+ self.exception = err
44
+ raise
45
+ ensure
46
+ unless row || !fd.is_a?(::File)
47
+ fd.close
48
+ @fd = nil
49
+ end
50
+
51
+ if exception
52
+ # Give some feedback if it crashes
53
+ msg = []
54
+ msg << "Last row IDX: #{idx}"
55
+ msg << "Last row content: #{row.to_h.pretty_inspect}"
56
+ puts msg
57
+ log(:debug) { msg.join("\n") }
58
+ end
59
+ end
60
+
19
61
  def for_each(start_at_idx: 0)
20
62
  raise ArgumentError, 'Expecting block, but not given.' unless block_given?
21
63
 
64
+ @started = true
22
65
  move_to_idx(start_at_idx)
23
66
 
24
67
  yield(row, next_idx) while (self.row = csv.shift)
@@ -38,6 +81,7 @@ module Eco
38
81
  end
39
82
 
40
83
  def move_to_idx(start_at_idx)
84
+ @started = true
41
85
  start_at_idx ||= 0
42
86
  next_idx while (idx < start_at_idx) && (self.row = csv.shift)
43
87
  end
@@ -58,12 +102,18 @@ module Eco
58
102
  return @csv if instance_variable_defined?(:@csv)
59
103
 
60
104
  @fd = ::File.open(filename, 'r')
61
- @csv = Eco::CSV.new(fd, **params)
105
+ @csv = Eco::CSV.new(fd, **params.slice(*csv_params))
62
106
  end
63
107
 
64
108
  def init
65
109
  @idx ||= 0 # rubocop:disable Naming/MemoizedInstanceVariableName
66
110
  end
111
+
112
+ def csv_params
113
+ return self.class::CSV_PARAMS if self.class.const_defined?(:CSV_PARAMS)
114
+
115
+ CSV_PARAMS
116
+ end
67
117
  end
68
118
  end
69
119
  end
data/lib/eco/csv.rb CHANGED
@@ -19,7 +19,7 @@ module Eco
19
19
  end
20
20
 
21
21
  # Splits the csv `filename` into `max_rows`
22
- # @yield [row, ridx, fidx, file]
22
+ # @yield [row, ridx, fidx, file] block to spot if the row should be included
23
23
  # @yieldparam row [Integer] the row
24
24
  # @yieldparam ridx [Integer] the index of the row in the source file
25
25
  # @yieldparam fidx [Integer] the number of the file
@@ -29,15 +29,18 @@ module Eco
29
29
  # @param max_rows [Integer] number of rows per file
30
30
  # @param start_at [Integer] row that sets the starting point.
31
31
  # Leave empty for the full set of rows.
32
+ # @param kargs [Hash] additional parameters
33
+ # - `:splitter` [Proc] custom splitter (criteria)
34
+ # - Receives the row idx and the row itself
32
35
  # @return [Eco::CSV::Split]
33
- def split(filename, max_rows:, start_at: nil, **kargs, &block)
36
+ def split(filename, max_rows: :unused, start_at: nil, **kargs, &filter)
34
37
  Eco::CSV::Split.new(
35
38
  filename,
36
39
  max_rows: max_rows,
37
40
  start_at: start_at,
38
41
  **kargs
39
42
  ).tap do |splitter|
40
- splitter.call(&block)
43
+ splitter.call(&filter)
41
44
  end
42
45
  end
43
46
 
data/lib/eco/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Eco
2
- VERSION = '3.2.12'.freeze
2
+ VERSION = '3.2.13'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eco-helpers
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.12
4
+ version: 3.2.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oscar Segura
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-01-19 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: byebug
@@ -536,7 +535,6 @@ dependencies:
536
535
  - - "~>"
537
536
  - !ruby/object:Gem::Version
538
537
  version: 6.7.0
539
- description:
540
538
  email:
541
539
  - oscar@ecoportal.co.nz
542
540
  executables: []
@@ -799,12 +797,14 @@ files:
799
797
  - lib/eco/api/usecases/default/utils/cli/entries_to_csv_cli.rb
800
798
  - lib/eco/api/usecases/default/utils/cli/group_csv_cli.rb
801
799
  - lib/eco/api/usecases/default/utils/cli/json_to_csv_cli.rb
800
+ - lib/eco/api/usecases/default/utils/cli/merge_csv_cli.rb
802
801
  - lib/eco/api/usecases/default/utils/cli/sort_csv_cli.rb
803
802
  - lib/eco/api/usecases/default/utils/cli/split_csv_cli.rb
804
803
  - lib/eco/api/usecases/default/utils/cli/split_json_cli.rb
805
804
  - lib/eco/api/usecases/default/utils/entries_to_csv_case.rb
806
805
  - lib/eco/api/usecases/default/utils/group_csv_case.rb
807
806
  - lib/eco/api/usecases/default/utils/json_to_csv_case.rb
807
+ - lib/eco/api/usecases/default/utils/merge_csv_case.rb
808
808
  - lib/eco/api/usecases/default/utils/sort_csv_case.rb
809
809
  - lib/eco/api/usecases/default/utils/split_csv_case.rb
810
810
  - lib/eco/api/usecases/default/utils/split_json_case.rb
@@ -1083,7 +1083,6 @@ licenses:
1083
1083
  - MIT
1084
1084
  metadata:
1085
1085
  rubygems_mfa_required: 'true'
1086
- post_install_message:
1087
1086
  rdoc_options: []
1088
1087
  require_paths:
1089
1088
  - lib
@@ -1098,8 +1097,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1098
1097
  - !ruby/object:Gem::Version
1099
1098
  version: '0'
1100
1099
  requirements: []
1101
- rubygems_version: 3.5.23
1102
- signing_key:
1100
+ rubygems_version: 4.0.8
1103
1101
  specification_version: 4
1104
1102
  summary: eco-helpers to manage people api cases
1105
1103
  test_files: []