embulk-guess-csv_verify 0.10.29-java

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 560fef03cacdda19b9365be2a7352d5a00c47b415fb89cd1f7ae6664d0603091
4
+ data.tar.gz: 79ab9de8f15c812cd3093207329674370d37563bdc05360f2652072a15afe12c
5
+ SHA512:
6
+ metadata.gz: ced3fb4071fa98eb20d4411272184217e9fc28e2c19e29192830d9fbcc158b67cdb87cfa7191c3fdf78794017009feef25982f60ae34bbae7cee08dc9257eafb
7
+ data.tar.gz: 357413c30817f29b0d2bdd1e22f093387374d4b1affff93e4ff88463f3468dfcfba4d1115da4f0f8daf48eb7f3a511ac012b0173af16a867fb1f2c63d9272bb6
Binary file
Binary file
@@ -0,0 +1,474 @@
1
+ module Embulk
2
+ module Guess
3
+ require 'embulk/guess/schema_guess'
4
+ require 'embulk/logger'
5
+
6
+ class CsvGuessPlugin < LineGuessPlugin
7
+ Plugin.register_guess('csv_verify', self)
8
+
9
+ def self.create_classloader
10
+ jars = Dir["#{File.expand_path('../../../../classpath', __FILE__)}/**/*.jar"]
11
+ urls = jars.map {|jar| java.io.File.new(File.expand_path(jar)).toURI.toURL }
12
+ begin
13
+ expected_temporary_variable_name = Java::org.embulk.jruby.JRubyPluginSource::PLUGIN_CLASS_LOADER_FACTORY_VARIABLE_NAME
14
+ rescue => e
15
+ raise PluginLoadError.new "Java's org.embulk.jruby.JRubyPluginSource does not define PLUGIN_CLASS_LOADER_FACTORY_VARIABLE_NAME unexpectedly."
16
+ end
17
+ if expected_temporary_variable_name != "$temporary_internal_plugin_class_loader_factory__"
18
+ raise PluginLoadError.new "Java's org.embulk.jruby.JRubyPluginSource does not define PLUGIN_CLASS_LOADER_FACTORY_VARIABLE_NAME correctly."
19
+ end
20
+ factory = $temporary_internal_plugin_class_loader_factory__
21
+ factory.create(urls, JRuby.runtime.getJRubyClassLoader())
22
+ end
23
+
24
+ CLASSLOADER = create_classloader
25
+ CONFIG_MAPPER_FACTORY_CLASS = CLASSLOADER.loadClass("org.embulk.util.config.ConfigMapperFactory").ruby_class
26
+ TYPE_MODULE_CLASS = CLASSLOADER.loadClass("org.embulk.util.config.modules.TypeModule").ruby_class
27
+ CONFIG_MAPPER_FACTORY = CONFIG_MAPPER_FACTORY_CLASS.builder.addDefaultModules.addModule(TYPE_MODULE_CLASS.new).build
28
+ PLUGIN_TASK_CLASS = CLASSLOADER.loadClass("org.embulk.parser.csv.CsvParserPlugin$PluginTask")
29
+ LIST_FILE_INPUT_CLASS = CLASSLOADER.loadClass("org.embulk.util.file.ListFileInput").ruby_class
30
+ LINE_DECODER_CLASS = CLASSLOADER.loadClass("org.embulk.util.text.LineDecoder").ruby_class
31
+ CSV_GUESS_PLUGIN_CLASS = CLASSLOADER.loadClass("org.embulk.guess.csv.CsvGuessPlugin").ruby_class
32
+ CSV_TOKENIZER_CLASS = CLASSLOADER.loadClass("org.embulk.parser.csv.CsvTokenizer").ruby_class
33
+ TOO_FEW_COLUMNS_EXCEPTION_CLASS = CLASSLOADER.loadClass("org.embulk.parser.csv.CsvTokenizer$TooFewColumnsException").ruby_class
34
+ INVALID_VALUE_EXCEPTION_CLASS = CLASSLOADER.loadClass("org.embulk.parser.csv.CsvTokenizer$InvalidValueException").ruby_class
35
+
36
+ DELIMITER_CANDIDATES = [
37
+ ",", "\t", "|", ";"
38
+ ]
39
+
40
+ QUOTE_CANDIDATES = [
41
+ "\"", "'"
42
+ ]
43
+
44
+ ESCAPE_CANDIDATES = [
45
+ "\\", '"'
46
+ ]
47
+
48
+ NULL_STRING_CANDIDATES = [
49
+ "null",
50
+ "NULL",
51
+ "#N/A",
52
+ "\\N", # MySQL LOAD, Hive STORED AS TEXTFILE
53
+ ]
54
+
55
+ COMMENT_LINE_MARKER_CANDIDATES = [
56
+ "#",
57
+ "//",
58
+ ]
59
+
60
+ MAX_SKIP_LINES = 10
61
+ NO_SKIP_DETECT_LINES = 10
62
+
63
+ def guess_lines(config, sample_lines)
64
+ guessed_ruby = guess_lines_iter(config, sample_lines)
65
+
66
+ begin
67
+ guess_plugin_java = CSV_GUESS_PLUGIN_CLASS.new
68
+ guessed_java = guess_plugin_java.guess_lines(config_to_java(config), config_to_java(sample_lines))
69
+ if guessed_java.nil?
70
+ raise "embulk-guess-csv (Java) returned null."
71
+ end
72
+ guessed_ruby_converted = config_to_java(guessed_ruby)
73
+ if !guessed_java.equals(guessed_ruby_converted)
74
+ raise_and_log_guess_diff(guessed_ruby, guessed_java)
75
+ end
76
+ rescue Exception => e
77
+ # Any error from the Java-based guess plugin should pass-through just with logging.
78
+ Embulk.logger.error "[Embulk CSV guess verify] #{e.inspect}"
79
+ end
80
+
81
+ # This plugin returns a result from the Ruby-based implementation.
82
+ return guessed_ruby
83
+ end
84
+
85
+ def guess_lines_iter(config, sample_lines)
86
+ return {} unless config.fetch("parser", {}).fetch("type", "csv") == "csv"
87
+
88
+ parser_config = config["parser"] || {}
89
+ if parser_config["type"] == "csv" && parser_config["delimiter"]
90
+ delim = parser_config["delimiter"]
91
+ else
92
+ delim = guess_delimiter(sample_lines)
93
+ unless delim
94
+ # assuming single column CSV
95
+ delim = DELIMITER_CANDIDATES.first
96
+ end
97
+ end
98
+
99
+ parser_guessed = DataSource.new.merge(parser_config).merge({"type" => "csv", "delimiter" => delim})
100
+
101
+ unless parser_guessed.has_key?("quote")
102
+ quote = guess_quote(sample_lines, delim)
103
+ unless quote
104
+ if !guess_force_no_quote(sample_lines, delim, '"')
105
+ # assuming CSV follows RFC for quoting
106
+ quote = '"'
107
+ else
108
+ # disable quoting (set null)
109
+ end
110
+ end
111
+ parser_guessed["quote"] = quote
112
+ end
113
+ parser_guessed["quote"] = '"' if parser_guessed["quote"] == '' # setting '' is not allowed any more. this line converts obsoleted config syntax to explicit syntax.
114
+
115
+ unless parser_guessed.has_key?("escape")
116
+ if quote = parser_guessed["quote"]
117
+ escape = guess_escape(sample_lines, delim, quote)
118
+ unless escape
119
+ if quote == '"'
120
+ # assuming this CSV follows RFC for escaping
121
+ escape = '"'
122
+ else
123
+ # disable escaping (set null)
124
+ end
125
+ end
126
+ parser_guessed["escape"] = escape
127
+ else
128
+ # escape does nothing if quote is disabled
129
+ end
130
+ end
131
+
132
+ unless parser_guessed.has_key?("null_string")
133
+ null_string = guess_null_string(sample_lines, delim)
134
+ parser_guessed["null_string"] = null_string if null_string
135
+ # don't even set null_string to avoid confusion of null and 'null' in YAML format
136
+ end
137
+
138
+ # guessing skip_header_lines should be before guessing guess_comment_line_marker
139
+ # because lines supplied to CsvTokenizer already don't include skipped header lines.
140
+ # skipping empty lines is also disabled here because skipping header lines is done by
141
+ # CsvParser which doesn't skip empty lines automatically
142
+ sample_records = split_lines(parser_guessed, false, sample_lines, delim, {})
143
+ skip_header_lines = guess_skip_header_lines(sample_records)
144
+ sample_lines = sample_lines[skip_header_lines..-1]
145
+ sample_records = sample_records[skip_header_lines..-1]
146
+
147
+ unless parser_guessed.has_key?("comment_line_marker")
148
+ comment_line_marker, sample_lines =
149
+ guess_comment_line_marker(sample_lines, delim, parser_guessed["quote"], parser_guessed["null_string"])
150
+ if comment_line_marker
151
+ parser_guessed["comment_line_marker"] = comment_line_marker
152
+ end
153
+ end
154
+
155
+ sample_records = split_lines(parser_guessed, true, sample_lines, delim, {})
156
+
157
+ # It should fail if CSV parser cannot parse sample_lines.
158
+ if sample_records.nil? || sample_records.empty?
159
+ return {}
160
+ end
161
+
162
+ if sample_lines.size == 1
163
+ # The file contains only 1 line. Assume that there are no header line.
164
+ header_line = false
165
+
166
+ column_types = SchemaGuess.types_from_array_records(sample_records[0, 1])
167
+
168
+ unless parser_guessed.has_key?("trim_if_not_quoted")
169
+ sample_records_trimmed = split_lines(parser_guessed, true, sample_lines, delim, {"trim_if_not_quoted" => true})
170
+ column_types_trimmed = SchemaGuess.types_from_array_records(sample_records_trimmed)
171
+ if column_types != column_types_trimmed
172
+ parser_guessed["trim_if_not_quoted"] = true
173
+ column_types = column_types_trimmed
174
+ else
175
+ parser_guessed["trim_if_not_quoted"] = false
176
+ end
177
+ end
178
+ else
179
+ # The file contains more than 1 line. If guessed first line's column types are all strings or boolean, and the types are
180
+ # different from the other lines, assume that the first line is column names.
181
+ first_types = SchemaGuess.types_from_array_records(sample_records[0, 1])
182
+ other_types = SchemaGuess.types_from_array_records(sample_records[1..-1] || [])
183
+
184
+ unless parser_guessed.has_key?("trim_if_not_quoted")
185
+ sample_records_trimmed = split_lines(parser_guessed, true, sample_lines, delim, {"trim_if_not_quoted" => true})
186
+ other_types_trimmed = SchemaGuess.types_from_array_records(sample_records_trimmed[1..-1] || [])
187
+ if other_types != other_types_trimmed
188
+ parser_guessed["trim_if_not_quoted"] = true
189
+ other_types = other_types_trimmed
190
+ else
191
+ parser_guessed["trim_if_not_quoted"] = false
192
+ end
193
+ end
194
+
195
+ header_line = (first_types != other_types && first_types.all? {|t| ["string", "boolean"].include?(t) }) || guess_string_header_line(sample_records)
196
+ column_types = other_types
197
+ end
198
+
199
+ if column_types.empty?
200
+ # TODO here is making the guessing failed if the file doesn't contain any columns. However,
201
+ # this may not be convenient for users.
202
+ return {}
203
+ end
204
+
205
+ if header_line
206
+ parser_guessed["skip_header_lines"] = skip_header_lines + 1
207
+ else
208
+ parser_guessed["skip_header_lines"] = skip_header_lines
209
+ end
210
+
211
+ parser_guessed["allow_extra_columns"] = false unless parser_guessed.has_key?("allow_extra_columns")
212
+ parser_guessed["allow_optional_columns"] = false unless parser_guessed.has_key?("allow_optional_columns")
213
+
214
+ if header_line
215
+ column_names = sample_records.first.map(&:strip)
216
+ else
217
+ column_names = (0..column_types.size).to_a.map {|i| "c#{i}" }
218
+ end
219
+ schema = []
220
+ column_names.zip(column_types).each do |name,type|
221
+ if name && type
222
+ schema << new_column(name, type)
223
+ end
224
+ end
225
+ parser_guessed["columns"] = schema
226
+
227
+ return {"parser" => parser_guessed}
228
+ end
229
+
230
+ def new_column(name, type)
231
+ if type.is_a?(SchemaGuess::TimestampTypeMatch)
232
+ {"name" => name, "type" => type, "format" => type.format}
233
+ else
234
+ {"name" => name, "type" => type}
235
+ end
236
+ end
237
+
238
+ private
239
+
240
+ def raise_and_log_guess_diff(guessed_ruby_entire, guessed_java_entire)
241
+ guessed_ruby = guessed_ruby_entire["parser"] || {}
242
+ guessed_java = guessed_java_entire.getNestedOrGetEmpty("parser")
243
+
244
+ require 'set'
245
+ keys = Set.new(guessed_ruby.keys) + Set.new(guessed_java.getAttributeNames)
246
+
247
+ begin
248
+ require 'json'
249
+ rescue LoadError
250
+ Embulk.logger.warn "The 'json' gem is not installed. No details compared."
251
+ guessed_java_hash = nil
252
+ else
253
+ guessed_java_hash = JSON.parse(guessed_java.toJson)
254
+ end
255
+
256
+ diffs = []
257
+ keys.each do |key|
258
+ if !guessed_ruby.has_key?(key)
259
+ diffs << "Only embulk-guess-csv (Java) has: \"#{key}\""
260
+ elsif !guessed_java.has(key.to_java)
261
+ diffs << "Only embulk-guess-csv (Ruby) has: \"#{key}\""
262
+ elsif guessed_java_hash && guessed_ruby[key] != guessed_java_hash[key]
263
+ diffs << "embulk-guess-csv has difference between Java/Ruby: \"#{key}\""
264
+ end
265
+ end
266
+
267
+ raise "embulk-guess-csv has difference between Java/Ruby: #{diffs.inspect}"
268
+ end
269
+
270
+ def config_to_java(config_ruby)
271
+ case config_ruby
272
+ when Hash then
273
+ config_java = CONFIG_MAPPER_FACTORY.newConfigSource
274
+ config_ruby.each do |key, value|
275
+ config_java.set(key.to_java, config_to_java(value))
276
+ end
277
+ return config_java
278
+ when Array then
279
+ config_java = Java::java.util.ArrayList.new
280
+ config_ruby.each do |v|
281
+ config_java.add(config_to_java(v))
282
+ end
283
+ return Java::java.util.Collections.unmodifiableList(config_java)
284
+ else
285
+ return config_ruby.to_java
286
+ end
287
+ end
288
+
289
+ def split_lines(parser_config, skip_empty_lines, sample_lines, delim, extra_config)
290
+ null_string = parser_config["null_string"]
291
+ config = parser_config.merge(extra_config).merge({"charset" => "UTF-8", "columns" => []})
292
+ parser_task = CONFIG_MAPPER_FACTORY.createConfigMapper.map(config_to_java(parser_config), PLUGIN_TASK_CLASS)
293
+ data = sample_lines.map {|line| line.force_encoding('UTF-8') }.join(parser_task.getNewline.getString.encode('UTF-8'))
294
+ sample = Buffer.from_ruby_string(data)
295
+ decoder = LINE_DECODER_CLASS.of(
296
+ LIST_FILE_INPUT_CLASS.new([[sample.to_java]]), parser_task.getCharset, parser_task.getLineDelimiterRecognized.orElse(nil))
297
+ tokenizer = CSV_TOKENIZER_CLASS.new(decoder, parser_task)
298
+ rows = []
299
+ while tokenizer.nextFile
300
+ while tokenizer.nextRecord(skip_empty_lines)
301
+ begin
302
+ columns = []
303
+ while true
304
+ begin
305
+ column = tokenizer.nextColumn
306
+ quoted = tokenizer.wasQuotedColumn
307
+ if null_string && !quoted && column == null_string
308
+ column = nil
309
+ end
310
+ columns << column
311
+ rescue TOO_FEW_COLUMNS_EXCEPTION_CLASS
312
+ rows << columns
313
+ break
314
+ end
315
+ end
316
+ rescue INVALID_VALUE_EXCEPTION_CLASS
317
+ # TODO warning
318
+ tokenizer.skipCurrentLine
319
+ end
320
+ end
321
+ end
322
+ return rows
323
+ rescue
324
+ # TODO warning if fallback to this ad-hoc implementation
325
+ sample_lines.map {|line| line.split(delim) }
326
+ end
327
+
328
+ def guess_delimiter(sample_lines)
329
+ delim_weights = DELIMITER_CANDIDATES.map do |d|
330
+ counts = sample_lines.map {|line| line.count(d) }
331
+ total = array_sum(counts)
332
+ if total > 0
333
+ stddev = array_standard_deviation(counts)
334
+ stddev = 0.000000001 if stddev == 0.0
335
+ weight = total / stddev
336
+ [d, weight]
337
+ else
338
+ [nil, 0]
339
+ end
340
+ end
341
+
342
+ delim, weight = *delim_weights.sort_by {|d,weight| weight }.last
343
+ if delim != nil && weight > 1
344
+ return delim
345
+ else
346
+ return nil
347
+ end
348
+ end
349
+
350
+ def guess_quote(sample_lines, delim)
351
+ delim_regexp = Regexp.escape(delim)
352
+ quote_weights = QUOTE_CANDIDATES.map do |q|
353
+ weights = sample_lines.map do |line|
354
+ q_regexp = Regexp.escape(q)
355
+ count = line.count(q)
356
+ if count > 0
357
+ weight = count
358
+ weight += line.scan(/(?:\A|#{delim_regexp})\s*#{q_regexp}(?:(?!#{q_regexp}).)*\s*#{q_regexp}(?:$|#{delim_regexp})/).size * 20
359
+ weight += line.scan(/(?:\A|#{delim_regexp})\s*#{q_regexp}(?:(?!#{delim_regexp}).)*\s*#{q_regexp}(?:$|#{delim_regexp})/).size * 40
360
+ weight
361
+ else
362
+ nil
363
+ end
364
+ end.compact
365
+ weights.empty? ? 0 : array_avg(weights)
366
+ end
367
+ quote, weight = QUOTE_CANDIDATES.zip(quote_weights).sort_by {|q,w| w }.last
368
+ if weight >= 10.0
369
+ return quote
370
+ else
371
+ return nil
372
+ end
373
+ end
374
+
375
+ def guess_force_no_quote(sample_lines, delim, quote_candidate)
376
+ delim_regexp = Regexp.escape(delim)
377
+ q_regexp = Regexp.escape(quote_candidate)
378
+ sample_lines.any? do |line|
379
+ # quoting character appear at the middle of a non-quoted value
380
+ line =~ /(?:\A|#{delim_regexp})\s*[^#{q_regexp}]+#{q_regexp}/
381
+ end
382
+ end
383
+
384
+ def guess_escape(sample_lines, delim, quote)
385
+ guessed = ESCAPE_CANDIDATES.map do |str|
386
+ regexp = /#{Regexp.quote(str)}(?:#{Regexp.quote(delim)}|#{Regexp.quote(quote)})/
387
+ counts = sample_lines.map {|line| line.scan(regexp).count }
388
+ count = counts.inject(0) {|r,c| r + c }
389
+ [str, count]
390
+ end.select {|str,count| count > 0 }.sort_by {|str,count| -count }
391
+ found = guessed.first
392
+ return found ? found[0] : nil
393
+ end
394
+
395
+ def guess_null_string(sample_lines, delim)
396
+ guessed = NULL_STRING_CANDIDATES.map do |str|
397
+ regexp = /(?:^|#{Regexp.quote(delim)})#{Regexp.quote(str)}(?:$|#{Regexp.quote(delim)})/
398
+ counts = sample_lines.map {|line| line.scan(regexp).count }
399
+ count = counts.inject(0) {|r,c| r + c }
400
+ [str, count]
401
+ end.select {|str,count| count > 0 }.sort_by {|str,count| -count }
402
+ found_str, found_count = guessed.first
403
+ return found_str ? found_str : nil
404
+ end
405
+
406
+ def guess_skip_header_lines(sample_records)
407
+ counts = sample_records.map {|records| records.size }
408
+ (1..[MAX_SKIP_LINES, counts.length - 1].min).each do |i|
409
+ check_row_count = counts[i-1]
410
+ if counts[i, NO_SKIP_DETECT_LINES].all? {|c| c <= check_row_count }
411
+ return i - 1
412
+ end
413
+ end
414
+ return 0
415
+ end
416
+
417
+ def guess_comment_line_marker(sample_lines, delim, quote, null_string)
418
+ exclude = []
419
+ exclude << /^#{Regexp.escape(quote)}/ if quote && !quote.empty?
420
+ exclude << /^#{Regexp.escape(null_string)}(?:#{Regexp.escape(delim)}|$)/ if null_string
421
+
422
+ guessed = COMMENT_LINE_MARKER_CANDIDATES.map do |str|
423
+ regexp = /^#{Regexp.quote(str)}/
424
+ unmatch_lines = sample_lines.reject do |line|
425
+ exclude.all? {|ex| line !~ ex } && line =~ regexp
426
+ end
427
+ match_count = sample_lines.size - unmatch_lines.size
428
+ [str, match_count, unmatch_lines]
429
+ end.select {|str,match_count,unmatch_lines| match_count > 0 }.sort_by {|str,match_count,unmatch_lines| -match_count }
430
+
431
+ str, match_count, unmatch_lines = guessed.first
432
+ if str
433
+ return str, unmatch_lines
434
+ else
435
+ return nil, sample_lines
436
+ end
437
+ end
438
+
439
+ def guess_string_header_line(sample_records)
440
+ first = sample_records.first
441
+ first.count.times do |column_index|
442
+ lengths = sample_records.map {|row| row[column_index] }.compact.map {|v| v.to_s.size }
443
+ if lengths.size > 1
444
+ if array_variance(lengths[1..-1]) <= 0.2
445
+ avg = array_avg(lengths[1..-1])
446
+ if avg == 0.0 ? lengths[0] > 1 : (avg - lengths[0]).abs / avg > 0.7
447
+ return true
448
+ end
449
+ end
450
+ end
451
+ end
452
+ return false
453
+ end
454
+
455
+ def array_sum(array)
456
+ array.inject(0) {|r,i| r += i }
457
+ end
458
+
459
+ def array_avg(array)
460
+ array.inject(0.0) {|r,i| r += i } / array.size
461
+ end
462
+
463
+ def array_variance(array)
464
+ avg = array_avg(array)
465
+ array.inject(0.0) {|r,i| r += (i - avg) ** 2 } / array.size
466
+ end
467
+
468
+ def array_standard_deviation(array)
469
+ Math.sqrt(array_variance(array))
470
+ end
471
+ end
472
+
473
+ end
474
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: embulk-guess-csv_verify
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.10.29
5
+ platform: java
6
+ authors:
7
+ - Sadayuki Furuhashi
8
+ - Muga Nishizawa
9
+ - Dai MIKURUBE
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2021-04-07 00:00:00.000000000 Z
14
+ dependencies: []
15
+ description: Verification-purpose Embulk CSV guess plugin to compare the old Ruby-based
16
+ one and the new Java-based one (not for your production use)
17
+ email:
18
+ - dmikurube@treasure-data.com
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - classpath/embulk-guess-csv-0.10.29.jar
24
+ - classpath/embulk-guess-csv_verify-0.10.29.jar
25
+ - classpath/embulk-parser-csv-0.10.29.jar
26
+ - classpath/embulk-util-config-0.2.1.jar
27
+ - classpath/embulk-util-file-0.1.3.jar
28
+ - classpath/embulk-util-guess-0.1.1.jar
29
+ - classpath/embulk-util-json-0.1.0.jar
30
+ - classpath/embulk-util-rubytime-0.3.2.jar
31
+ - classpath/embulk-util-text-0.1.0.jar
32
+ - classpath/embulk-util-timestamp-0.2.1.jar
33
+ - classpath/icu4j-54.1.1.jar
34
+ - classpath/jackson-annotations-2.6.7.jar
35
+ - classpath/jackson-core-2.6.7.jar
36
+ - classpath/jackson-databind-2.6.7.jar
37
+ - classpath/jackson-datatype-jdk8-2.6.7.jar
38
+ - classpath/validation-api-1.1.0.Final.jar
39
+ - lib/embulk/guess/csv_verify.rb
40
+ homepage: https://github.com/embulk/embulk
41
+ licenses:
42
+ - Apache-2.0
43
+ metadata: {}
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubyforge_project:
60
+ rubygems_version: 2.7.9
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: Verification-purpose Embulk CSV guess plugin (not for your production use)
64
+ test_files: []