iostreams 1.6.0 → 1.8.0

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: e4fb750e5c3779000fac8f21803b84a5977f111f481b7d4e11a117149cd0d9e6
4
- data.tar.gz: '09cb2dc7ff67afd44d3ebcfefa29f6a1840d1b3dd871c33bc1a06914cd8bee2d'
3
+ metadata.gz: b84445b48ac6697a255b5fdacd16783b8573e2cdb75ad395c3b4167ffdd6e01b
4
+ data.tar.gz: a37969afc9635fdac60ea45717d64b9bdd8fbb7c6d853a04c0a02cd798274606
5
5
  SHA512:
6
- metadata.gz: 0cf2db14e03b9e81e0e39119b35f293408a9d4b6bf3365abc724d95d7376abf7af73720b8fd8d000b70b2eb7abbae20055753e845a77dec2d8d7297e5b6693ba
7
- data.tar.gz: d9fa2194965ef99a99e1ecd0655e84066e6021236d1494336c40693b103d3c7ecb7e5a87e8f179a38c990ad7670d6bd4901b6a7cd8b89757c00a3179d657a1cc
6
+ metadata.gz: 685d6a23dfc176f3abe922fab1879c76572b73dc878864af3e38dfa5824e0eb8c115f855537dcf3c3f2d181b3381512a2c9a20c065beb62cfd91642913c84b30
7
+ data.tar.gz: db6e6fcb72c07fe502e64f9ab4d18c79334cde6c1622d1b2ac2a22361ed1f38967adc19bb83959e56fc2039ee595f078c1a92d6277a5b8d39d3802e020df0a1a
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # IOStreams
2
- [![Gem Version](https://img.shields.io/gem/v/iostreams.svg)](https://rubygems.org/gems/iostreams) [![Downloads](https://img.shields.io/gem/dt/iostreams.svg)](https://rubygems.org/gems/iostreams) [![License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](http://opensource.org/licenses/Apache-2.0) ![](https://img.shields.io/badge/status-Production%20Ready-blue.svg) [![Gitter chat](https://img.shields.io/badge/IRC%20(gitter)-Support-brightgreen.svg)](https://gitter.im/rocketjob/support)
2
+ [![Gem Version](https://img.shields.io/gem/v/iostreams.svg)](https://rubygems.org/gems/iostreams) [![Downloads](https://img.shields.io/gem/dt/iostreams.svg)](https://rubygems.org/gems/iostreams) [![License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](http://opensource.org/licenses/Apache-2.0) ![](https://img.shields.io/badge/status-Production%20Ready-blue.svg)
3
3
 
4
4
  IOStreams is an incredibly powerful streaming library that makes changes to file formats, compression, encryption,
5
5
  or storage mechanism transparent to the application.
@@ -97,7 +97,9 @@ module IOStreams
97
97
  end
98
98
 
99
99
  def format=(format)
100
- raise(ArgumentError, "Invalid format: #{format.inspect}") unless format.nil? || IOStreams::Tabular.registered_formats.include?(format)
100
+ unless format.nil? || IOStreams::Tabular.registered_formats.include?(format)
101
+ raise(ArgumentError, "Invalid format: #{format.inspect}")
102
+ end
101
103
 
102
104
  @format = format
103
105
  end
@@ -106,7 +108,7 @@ module IOStreams
106
108
 
107
109
  def class_for_stream(type, stream)
108
110
  ext = IOStreams.extensions[stream.nil? ? nil : stream.to_sym] ||
109
- raise(ArgumentError, "Unknown Stream type: #{stream.inspect}")
111
+ raise(ArgumentError, "Unknown Stream type: #{stream.inspect}")
110
112
  ext.send("#{type}_class") || raise(ArgumentError, "No #{type} registered for Stream type: #{stream.inspect}")
111
113
  end
112
114
 
@@ -96,17 +96,17 @@ module IOStreams
96
96
  while line.count(@embedded_within).odd?
97
97
  if eof? || line.length > @buffer_size * 10
98
98
  raise(Errors::MalformedDataError.new(
99
- "Unbalanced delimited field, delimiter: #{@embedded_within}",
100
- initial_line_number
101
- ))
99
+ "Unbalanced delimited field, delimiter: #{@embedded_within}",
100
+ initial_line_number
101
+ ))
102
102
  end
103
103
  line << @delimiter
104
104
  next_line = _readline
105
105
  if next_line.nil?
106
106
  raise(Errors::MalformedDataError.new(
107
- "Unbalanced delimited field, delimiter: #{@embedded_within}",
108
- initial_line_number
109
- ))
107
+ "Unbalanced delimited field, delimiter: #{@embedded_within}",
108
+ initial_line_number
109
+ ))
110
110
  end
111
111
  line << next_line
112
112
  end
@@ -3,7 +3,7 @@ require "uri"
3
3
  module IOStreams
4
4
  module Paths
5
5
  class S3 < IOStreams::Path
6
- attr_reader :bucket_name, :client, :options
6
+ attr_reader :bucket_name, :options
7
7
 
8
8
  # Largest file size supported by the S3 copy object api.
9
9
  S3_COPY_OBJECT_SIZE_LIMIT = 5 * 1024 * 1024 * 1024
@@ -141,16 +141,17 @@ module IOStreams
141
141
 
142
142
  @bucket_name = uri.hostname
143
143
  key = uri.path.sub(%r{\A/}, "")
144
- if client.is_a?(Hash)
145
- client[:access_key_id] = access_key_id if access_key_id
146
- client[:secret_access_key] = secret_access_key if secret_access_key
147
- @client = ::Aws::S3::Client.new(client)
144
+
145
+ if client && !client.is_a?(Hash)
146
+ @client = client
148
147
  else
149
- @client = client || ::Aws::S3::Client.new(access_key_id: access_key_id, secret_access_key: secret_access_key)
148
+ @client_options = client.is_a?(Hash) ? client.dup : {}
149
+ @client_options[:access_key_id] = access_key_id if access_key_id
150
+ @client_options[:secret_access_key] = secret_access_key if secret_access_key
150
151
  end
151
- @options = args
152
152
 
153
- @options.merge(uri.query) if uri.query
153
+ @options = args
154
+ @options.merge!(uri.query.transform_keys(&:to_sym)) if uri.query
154
155
 
155
156
  super(key)
156
157
  end
@@ -190,11 +191,11 @@ module IOStreams
190
191
  end
191
192
 
192
193
  # Make S3 perform direct copies within S3 itself.
193
- def copy_to(target_path, convert: true)
194
- return super(target_path) if convert || (size.to_i >= S3_COPY_OBJECT_SIZE_LIMIT)
194
+ def copy_to(target_path, convert: true, **args)
195
+ return super(target_path, convert: convert, **args) if convert || (size.to_i >= S3_COPY_OBJECT_SIZE_LIMIT)
195
196
 
196
197
  target = IOStreams.new(target_path)
197
- return super(target) unless target.is_a?(self.class)
198
+ return super(target, convert: convert, **args) unless target.is_a?(self.class)
198
199
 
199
200
  source_name = ::File.join(bucket_name, path)
200
201
  client.copy_object(options.merge(bucket: target.bucket_name, key: target.path, copy_source: source_name))
@@ -202,11 +203,13 @@ module IOStreams
202
203
  end
203
204
 
204
205
  # Make S3 perform direct copies within S3 itself.
205
- def copy_from(source_path, convert: true)
206
- return super(source_path) if convert
206
+ def copy_from(source_path, convert: true, **args)
207
+ return super(source_path, convert: true, **args) if convert
207
208
 
208
209
  source = IOStreams.new(source_path)
209
- return super(source) if !source.is_a?(self.class) || (source.size.to_i >= S3_COPY_OBJECT_SIZE_LIMIT)
210
+ if !source.is_a?(self.class) || (source.size.to_i >= S3_COPY_OBJECT_SIZE_LIMIT)
211
+ return super(source, convert: convert, **args)
212
+ end
210
213
 
211
214
  source_name = ::File.join(source.bucket_name, source.path)
212
215
  client.copy_object(options.merge(bucket: bucket_name, key: path, copy_source: source_name))
@@ -312,6 +315,11 @@ module IOStreams
312
315
  def partial_files_visible?
313
316
  false
314
317
  end
318
+
319
+ # Lazy load S3 client since it takes two seconds to create itself!
320
+ def client
321
+ @client ||= ::Aws::S3::Client.new(@client_options)
322
+ end
315
323
  end
316
324
  end
317
325
  end
@@ -26,12 +26,13 @@ module IOStreams
26
26
  include SemanticLogger::Loggable if defined?(SemanticLogger)
27
27
 
28
28
  class << self
29
- attr_accessor :sshpass_bin, :sftp_bin, :sshpass_wait_seconds
29
+ attr_accessor :sshpass_bin, :sftp_bin, :sshpass_wait_seconds, :before_password_wait_seconds
30
30
  end
31
31
 
32
- @sftp_bin = "sftp"
33
- @sshpass_bin = "sshpass"
34
- @sshpass_wait_seconds = 5
32
+ @sftp_bin = "sftp"
33
+ @sshpass_bin = "sshpass"
34
+ @before_password_wait_seconds = 2
35
+ @sshpass_wait_seconds = 5
35
36
 
36
37
  attr_reader :hostname, :username, :ssh_options, :url, :port
37
38
 
@@ -46,9 +47,23 @@ module IOStreams
46
47
  # password: [String]
47
48
  # Password for the user.
48
49
  #
49
- # **ssh_options
50
- # Any other options supported by ssh_config.
51
- # `man ssh_config` to see all available options.
50
+ # ssh_options: [Hash]
51
+ # - IdentityKey [String]
52
+ # The identity key that this client should use to talk to this host.
53
+ # Under the covers this value is written to a file and then the file name is passed as `IdentityFile`
54
+ # - HostKey [String]
55
+ # The expected SSH Host key that is presented by the remote host.
56
+ # Instead of storing the host key in the `known_hosts` file, it can be supplied explicity
57
+ # using this option.
58
+ # Under the covers this value is written to a file and then the file name is passed as `UserKnownHostsFile`
59
+ # Notes:
60
+ # - It must contain the entire line that would be stored in `known_hosts`,
61
+ # including the hostname, ip address, key type and key value. This value is written as-is into a
62
+ # "known_hosts" like file and then passed into sftp using the `UserKnownHostsFile` option.
63
+ # - The easiest way to generate the required is to use `ssh-keyscan` and then supply that value in this field.
64
+ # For example: `ssh-keyscan hostname`
65
+ # - Any other options supported by ssh_config.
66
+ # `man ssh_config` to see all available options.
52
67
  #
53
68
  # Examples:
54
69
  #
@@ -167,33 +182,36 @@ module IOStreams
167
182
  def sftp_download(remote_file_name, local_file_name)
168
183
  with_sftp_args do |args|
169
184
  Open3.popen2e(*args) do |writer, reader, waith_thr|
170
- begin
171
- writer.puts password
172
- # Give time for password to be processed and stdin to be passed to sftp process.
173
- sleep self.class.sshpass_wait_seconds
174
- writer.puts "get #{remote_file_name} #{local_file_name}"
175
- writer.puts "bye"
176
- writer.close
177
- out = reader.read.chomp
178
- unless waith_thr.value.success?
179
- raise(
180
- Errors::CommunicationsFailure,
181
- "Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
182
- )
183
- end
184
-
185
- out
186
- rescue Errno::EPIPE
187
- out = begin
188
- reader.read.chomp
189
- rescue StandardError
190
- nil
191
- end
185
+ # Give time for remote sftp server to get ready to accept the password.
186
+ sleep self.class.before_password_wait_seconds
187
+
188
+ writer.puts password
189
+
190
+ # Give time for password to be processed and stdin to be passed to sftp process.
191
+ sleep self.class.sshpass_wait_seconds
192
+
193
+ writer.puts "get #{remote_file_name} #{local_file_name}"
194
+ writer.puts "bye"
195
+ writer.close
196
+ out = reader.read.chomp
197
+ unless waith_thr.value.success?
192
198
  raise(
193
199
  Errors::CommunicationsFailure,
194
200
  "Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
195
201
  )
196
202
  end
203
+
204
+ out
205
+ rescue Errno::EPIPE
206
+ out = begin
207
+ reader.read.chomp
208
+ rescue StandardError
209
+ nil
210
+ end
211
+ raise(
212
+ Errors::CommunicationsFailure,
213
+ "Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
214
+ )
197
215
  end
198
216
  end
199
217
  end
@@ -201,48 +219,64 @@ module IOStreams
201
219
  def sftp_upload(local_file_name, remote_file_name)
202
220
  with_sftp_args do |args|
203
221
  Open3.popen2e(*args) do |writer, reader, waith_thr|
204
- begin
205
- writer.puts(password) if password
206
- # Give time for password to be processed and stdin to be passed to sftp process.
207
- sleep self.class.sshpass_wait_seconds
208
- writer.puts "put #{local_file_name.inspect} #{remote_file_name.inspect}"
209
- writer.puts "bye"
210
- writer.close
211
- out = reader.read.chomp
212
- unless waith_thr.value.success?
213
- raise(
214
- Errors::CommunicationsFailure,
215
- "Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
216
- )
217
- end
218
-
219
- out
220
- rescue Errno::EPIPE
221
- out = begin
222
- reader.read.chomp
223
- rescue StandardError
224
- nil
225
- end
222
+ writer.puts(password) if password
223
+ # Give time for password to be processed and stdin to be passed to sftp process.
224
+ sleep self.class.sshpass_wait_seconds
225
+ writer.puts "put #{local_file_name.inspect} #{remote_file_name.inspect}"
226
+ writer.puts "bye"
227
+ writer.close
228
+ out = reader.read.chomp
229
+ unless waith_thr.value.success?
226
230
  raise(
227
231
  Errors::CommunicationsFailure,
228
232
  "Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
229
233
  )
230
234
  end
235
+
236
+ out
237
+ rescue Errno::EPIPE
238
+ out = begin
239
+ reader.read.chomp
240
+ rescue StandardError
241
+ nil
242
+ end
243
+ raise(
244
+ Errors::CommunicationsFailure,
245
+ "Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}"
246
+ )
231
247
  end
232
248
  end
233
249
  end
234
250
 
235
251
  def with_sftp_args
236
- return yield sftp_args(ssh_options) unless ssh_options.key?("IdentityKey")
252
+ return yield sftp_args(ssh_options) if !ssh_options.key?("IdentityKey") && !ssh_options.key?("HostKey")
253
+
254
+ with_identity_key(ssh_options.dup) do |options|
255
+ with_host_key(options) do |options2|
256
+ yield sftp_args(options2)
257
+ end
258
+ end
259
+ end
260
+
261
+ def with_identity_key(options)
262
+ return yield options unless ssh_options.key?("IdentityKey")
263
+
264
+ with_temp_file(options, "IdentityFile", options.delete("IdentityKey")) { yield options }
265
+ end
266
+
267
+ def with_host_key(options)
268
+ return yield options unless ssh_options.key?("HostKey")
269
+
270
+ with_temp_file(options, "UserKnownHostsFile", options.delete("HostKey")) { yield options }
271
+ end
237
272
 
273
+ def with_temp_file(options, option, value)
238
274
  Utils.temp_file_name("iostreams-sftp-args", "key") do |file_name|
239
- options = ssh_options.dup
240
- key = options.delete("IdentityKey")
241
275
  # sftp requires that private key is only readable by the current user
242
- ::File.open(file_name, "wb", 0o600) { |io| io.write(key) }
276
+ ::File.open(file_name, "wb", 0o600) { |io| io.write(value) }
243
277
 
244
- options["IdentityFile"] = file_name
245
- yield sftp_args(options)
278
+ options[option] = file_name
279
+ yield options
246
280
  end
247
281
  end
248
282
 
@@ -272,8 +306,8 @@ module IOStreams
272
306
 
273
307
  def build_ssh_options
274
308
  options = ssh_options.dup
275
- options[:logger] ||= logger if defined?(SemanticLogger)
276
- options[:port] ||= port
309
+ options[:logger] ||= logger if defined?(SemanticLogger)
310
+ options[:port] ||= port
277
311
  options[:max_pkt_size] ||= 65_536
278
312
  options[:password] ||= @password
279
313
  options
@@ -48,8 +48,8 @@ module IOStreams
48
48
  # See `man gpg` for the remaining options
49
49
  def self.generate_key(name:,
50
50
  email:,
51
- comment: nil,
52
51
  passphrase:,
52
+ comment: nil,
53
53
  key_type: "RSA",
54
54
  key_length: 4096,
55
55
  subkey_type: "RSA",
@@ -261,7 +261,7 @@ module IOStreams
261
261
 
262
262
  import(key: key)
263
263
  set_trust(email: email, key_id: key_id)
264
- email
264
+ email || key_id
265
265
  end
266
266
 
267
267
  # Set the trust level for an existing key.
@@ -291,10 +291,8 @@ module IOStreams
291
291
  version_check
292
292
  Open3.popen2e("#{executable} --list-keys --fingerprint --with-colons #{email}") do |_stdin, out, waith_thr|
293
293
  output = out.read.chomp
294
- unless waith_thr.value.success?
295
- unless output =~ /(public key not found|No public key)/i
296
- raise(Pgp::Failure, "GPG Failed calling #{executable} to list keys for #{email}: #{output}")
297
- end
294
+ if !waith_thr.value.success? && !(output !~ /(public key not found|No public key)/i)
295
+ raise(Pgp::Failure, "GPG Failed calling #{executable} to list keys for #{email}: #{output}")
298
296
  end
299
297
 
300
298
  output.each_line do |line|
@@ -336,9 +334,11 @@ module IOStreams
336
334
  match[1]
337
335
  end
338
336
  else
339
- return [] if err =~ /(key not found|No (public|secret) key)/i
337
+ if err !~ /(key not found|No (public|secret) key)/i
338
+ raise(Pgp::Failure, "GPG Failed calling #{executable} to list keys for #{email || key_id}: #{err}#{out}")
339
+ end
340
340
 
341
- raise(Pgp::Failure, "GPG Failed calling #{executable} to list keys for #{email || key_id}: #{err}#{out}")
341
+ []
342
342
  end
343
343
  end
344
344
  end
@@ -350,7 +350,7 @@ module IOStreams
350
350
  end
351
351
 
352
352
  def self.version_check
353
- return unless pgp_version.to_f >= 2.3
353
+ return unless pgp_version.to_f >= 2.4
354
354
 
355
355
  raise(
356
356
  Pgp::UnsupportedVersion,
@@ -382,10 +382,10 @@ module IOStreams
382
382
  key_length: match[3].to_s.to_i,
383
383
  key_type: match[2],
384
384
  date: (begin
385
- Date.parse(match[4].to_s)
386
- rescue StandardError
387
- match[4]
388
- end)
385
+ Date.parse(match[4].to_s)
386
+ rescue StandardError
387
+ match[4]
388
+ end)
389
389
  }
390
390
  elsif (match = line.match(%r{(pub|sec)\s+(\d+)(.*)/(\w+)\s+(\d+-\d+-\d+)(\s+(.+)<(.+)>)?}))
391
391
  # Matches: pub 2048R/C7F9D9CB 2016-10-26
@@ -396,10 +396,10 @@ module IOStreams
396
396
  key_type: match[3],
397
397
  key_id: match[4],
398
398
  date: (begin
399
- Date.parse(match[5].to_s)
400
- rescue StandardError
401
- match[5]
402
- end)
399
+ Date.parse(match[5].to_s)
400
+ rescue StandardError
401
+ match[5]
402
+ end)
403
403
  }
404
404
  # Prior to gpg v2.0.30
405
405
  if match[7]
@@ -19,8 +19,7 @@ module IOStreams
19
19
 
20
20
  private
21
21
 
22
- attr_reader :default_signer_passphrase
23
- attr_reader :default_signer
22
+ attr_reader :default_signer_passphrase, :default_signer
24
23
 
25
24
  @default_signer_passphrase = nil
26
25
  @default_signer = nil
@@ -151,6 +151,9 @@ module IOStreams
151
151
  # Whether to apply the stream conversions during the copy.
152
152
  # Default: true
153
153
  #
154
+ # :mode [:line, :array, :hash]
155
+ # When convert is `true` then use this mode to convert the contents of the file.
156
+ #
154
157
  # Examples:
155
158
  #
156
159
  # # Copy and convert streams based on file extensions
@@ -162,11 +165,17 @@ module IOStreams
162
165
  # # Advanced copy with custom stream conversions on source and target.
163
166
  # source = IOStreams.path("source_file").stream(encoding: "BINARY")
164
167
  # IOStreams.path("target_file.pgp").option(:pgp, passphrase: "hello").copy_from(source)
165
- def copy_from(source, convert: true)
168
+ def copy_from(source, convert: true, mode: nil, **args)
166
169
  if convert
167
170
  stream = IOStreams.new(source)
168
- writer do |target|
169
- stream.reader { |src| IO.copy_stream(src, target) }
171
+ if mode
172
+ writer(mode, **args) do |target|
173
+ stream.each(mode) { |row| target << row }
174
+ end
175
+ else
176
+ writer(**args) do |target|
177
+ stream.reader { |src| IO.copy_stream(src, target) }
178
+ end
170
179
  end
171
180
  else
172
181
  stream = source.is_a?(Stream) ? source.dup : IOStreams.new(source)
@@ -176,9 +185,9 @@ module IOStreams
176
185
  end
177
186
  end
178
187
 
179
- def copy_to(target, convert: true)
188
+ def copy_to(target, **args)
180
189
  target = IOStreams.new(target)
181
- target.copy_from(self, convert: convert)
190
+ target.copy_from(self, **args)
182
191
  end
183
192
 
184
193
  # Set/get the original file_name
@@ -365,8 +374,8 @@ module IOStreams
365
374
  IOStreams::Row::Writer.stream(
366
375
  io,
367
376
  original_file_name: builder.file_name,
368
- format: builder.format,
369
- format_options: builder.format_options,
377
+ format: builder.format,
378
+ format_options: builder.format_options,
370
379
  **args,
371
380
  &block
372
381
  )
@@ -380,10 +389,11 @@ module IOStreams
380
389
  IOStreams::Record::Writer.stream(
381
390
  io,
382
391
  original_file_name: builder.file_name,
383
- format: builder.format,
384
- format_options: builder.format_options,
392
+ format: builder.format,
393
+ format_options: builder.format_options,
385
394
  **args,
386
- &block)
395
+ &block
396
+ )
387
397
  end
388
398
  end
389
399
  end
@@ -2,6 +2,9 @@ module IOStreams
2
2
  class Tabular
3
3
  # Process files / streams that start with a header.
4
4
  class Header
5
+ # Column names that begin with this prefix have been rejected and should be ignored.
6
+ IGNORE_PREFIX = "__rejected__".freeze
7
+
5
8
  attr_accessor :columns, :allowed_columns, :required_columns, :skip_unknown
6
9
 
7
10
  # Header
@@ -17,8 +20,8 @@ module IOStreams
17
20
  # List of columns to allow.
18
21
  # Default: nil ( Allow all columns )
19
22
  # Note:
20
- # When supplied any columns that are rejected will be returned in the cleansed columns
21
- # as nil so that they can be ignored during processing.
23
+ # * So that rejected columns can be identified in subsequent steps, they will be prefixed with `__rejected__`.
24
+ # For example, `Unknown Column` would be cleansed as `__rejected__Unknown Column`.
22
25
  #
23
26
  # required_columns [Array<String>]
24
27
  # List of columns that must be present, otherwise an Exception is raised.
@@ -44,8 +47,10 @@ module IOStreams
44
47
  # - Spaces and '-' are converted to '_'.
45
48
  # - All characters except for letters, digits, and '_' are stripped.
46
49
  #
47
- # Notes
48
- # * Raises Tabular::InvalidHeader when there are no non-nil columns left after cleansing.
50
+ # Notes:
51
+ # * So that rejected columns can be identified in subsequent steps, they will be prefixed with `__rejected__`.
52
+ # For example, `Unknown Column` would be cleansed as `__rejected__Unknown Column`.
53
+ # * Raises Tabular::InvalidHeader when there are no rejected columns left after cleansing.
49
54
  def cleanse!
50
55
  return [] if columns.nil? || columns.empty?
51
56
 
@@ -56,7 +61,7 @@ module IOStreams
56
61
  cleansed
57
62
  else
58
63
  ignored_columns << column
59
- nil
64
+ "#{IGNORE_PREFIX}#{column}"
60
65
  end
61
66
  end
62
67
 
@@ -122,7 +127,7 @@ module IOStreams
122
127
 
123
128
  def array_to_hash(row)
124
129
  h = {}
125
- columns.each_with_index { |col, i| h[col] = row[i] unless IOStreams::Utils.blank?(col) }
130
+ columns.each_with_index { |col, i| h[col] = row[i] unless IOStreams::Utils.blank?(col) || col.start_with?(IGNORE_PREFIX) }
126
131
  h
127
132
  end
128
133
 
@@ -134,12 +139,7 @@ module IOStreams
134
139
  hash = hash.dup
135
140
  unmatched.each { |name| hash[cleanse_column(name)] = hash.delete(name) }
136
141
  end
137
- # Hash#slice as of Ruby 2.5
138
- if hash.respond_to?(:slice)
139
- hash.slice(*columns)
140
- else
141
- columns.each_with_object({}) { |column, new_hash| new_hash[column] = hash[column] }
142
- end
142
+ hash.slice(*columns)
143
143
  end
144
144
 
145
145
  def cleanse_column(name)
@@ -146,7 +146,7 @@ module IOStreams
146
146
 
147
147
  attr_reader :key, :size, :type, :decimals
148
148
 
149
- def initialize(key: nil, size:, type: :string, decimals: 2)
149
+ def initialize(size:, key: nil, type: :string, decimals: 2)
150
150
  @key = key
151
151
  @size = size == :remainder ? -1 : size.to_i
152
152
  @type = type.to_sym
@@ -28,11 +28,9 @@ module IOStreams
28
28
  def self.temp_file_name(basename, extension = "")
29
29
  result = nil
30
30
  ::Dir::Tmpname.create([basename, extension], IOStreams.temp_dir, max_try: MAX_TEMP_FILE_NAME_ATTEMPTS) do |tmpname|
31
- begin
32
- result = yield(tmpname)
33
- ensure
34
- ::File.unlink(tmpname) if ::File.exist?(tmpname)
35
- end
31
+ result = yield(tmpname)
32
+ ensure
33
+ ::File.unlink(tmpname) if ::File.exist?(tmpname)
36
34
  end
37
35
  result
38
36
  end
@@ -1,3 +1,3 @@
1
1
  module IOStreams
2
- VERSION = "1.6.0".freeze
2
+ VERSION = "1.8.0".freeze
3
3
  end
data/lib/iostreams.rb CHANGED
@@ -23,33 +23,41 @@ module IOStreams
23
23
  autoload :Reader, "io_streams/bzip2/reader"
24
24
  autoload :Writer, "io_streams/bzip2/writer"
25
25
  end
26
+
26
27
  module Encode
27
28
  autoload :Reader, "io_streams/encode/reader"
28
29
  autoload :Writer, "io_streams/encode/writer"
29
30
  end
31
+
30
32
  module Gzip
31
33
  autoload :Reader, "io_streams/gzip/reader"
32
34
  autoload :Writer, "io_streams/gzip/writer"
33
35
  end
36
+
34
37
  module Line
35
38
  autoload :Reader, "io_streams/line/reader"
36
39
  autoload :Writer, "io_streams/line/writer"
37
40
  end
41
+
38
42
  module Record
39
43
  autoload :Reader, "io_streams/record/reader"
40
44
  autoload :Writer, "io_streams/record/writer"
41
45
  end
46
+
42
47
  module Row
43
48
  autoload :Reader, "io_streams/row/reader"
44
49
  autoload :Writer, "io_streams/row/writer"
45
50
  end
51
+
46
52
  module SymmetricEncryption
47
53
  autoload :Reader, "io_streams/symmetric_encryption/reader"
48
54
  autoload :Writer, "io_streams/symmetric_encryption/writer"
49
55
  end
56
+
50
57
  module Xlsx
51
58
  autoload :Reader, "io_streams/xlsx/reader"
52
59
  end
60
+
53
61
  module Zip
54
62
  autoload :Reader, "io_streams/zip/reader"
55
63
  autoload :Writer, "io_streams/zip/writer"
@@ -126,15 +126,13 @@ module Paths
126
126
 
127
127
  it "missing source file" do
128
128
  IOStreams.temp_file("iostreams_move_test", ".txt") do |temp_file|
129
- begin
130
- refute temp_file.exist?
131
- target = temp_file.directory.join("move_test.txt")
132
- assert_raises Errno::ENOENT do
133
- temp_file.move_to(target)
134
- end
135
- refute target.exist?
136
- refute temp_file.exist?
129
+ refute temp_file.exist?
130
+ target = temp_file.directory.join("move_test.txt")
131
+ assert_raises Errno::ENOENT do
132
+ temp_file.move_to(target)
137
133
  end
134
+ refute target.exist?
135
+ refute temp_file.exist?
138
136
  end
139
137
  end
140
138
 
@@ -20,7 +20,13 @@ module Paths
20
20
  let(:file_name) { File.join(File.dirname(__FILE__), "..", "files", "text file.txt") }
21
21
  let(:raw) { File.read(file_name) }
22
22
 
23
- let(:root_path) { IOStreams::Paths::SFTP.new(url, username: username, password: password) }
23
+ let(:root_path) do
24
+ if ENV["SFTP_HOST_KEY"]
25
+ IOStreams::Paths::SFTP.new(url, username: username, password: password, ssh_options: {"HostKey" => ENV["SFTP_HOST_KEY"]})
26
+ else
27
+ IOStreams::Paths::SFTP.new(url, username: username, password: password)
28
+ end
29
+ end
24
30
 
25
31
  let :existing_path do
26
32
  path = root_path.join("test.txt")
data/test/stream_test.rb CHANGED
@@ -45,9 +45,9 @@ class StreamTest < Minitest::Test
45
45
  it "reads a zip file" do
46
46
  File.open(multiple_zip_file_name, "rb") do |io|
47
47
  result = IOStreams::Stream.new(io).
48
- file_name(multiple_zip_file_name).
49
- option(:zip, entry_file_name: "test.json").
50
- read
48
+ file_name(multiple_zip_file_name).
49
+ option(:zip, entry_file_name: "test.json").
50
+ read
51
51
  assert_equal contents_test_json, result
52
52
  end
53
53
  end
@@ -55,8 +55,8 @@ class StreamTest < Minitest::Test
55
55
  it "reads a zip file from within a gz file" do
56
56
  File.open(zip_gz_file_name, "rb") do |io|
57
57
  result = IOStreams::Stream.new(io).
58
- file_name(zip_gz_file_name).
59
- read
58
+ file_name(zip_gz_file_name).
59
+ read
60
60
  assert_equal contents_test_txt, result
61
61
  end
62
62
  end
data/test/tabular_test.rb CHANGED
@@ -58,12 +58,12 @@ class TabularTest < Minitest::Test
58
58
  assert_equal header, tabular.header.columns
59
59
  end
60
60
 
61
- it "white listed snake cased alphanumeric columns" do
61
+ it "allowed list snake cased alphanumeric columns" do
62
62
  tabular = IOStreams::Tabular.new(
63
- columns: ["Ard Vark", "password", "robot version", "$$$"],
63
+ columns: ["Ard Vark", "Password", "robot version", "$$$"],
64
64
  allowed_columns: %w[ard_vark robot_version]
65
65
  )
66
- expected_header = ["ard_vark", nil, "robot_version", nil]
66
+ expected_header = ["ard_vark", "__rejected__Password", "robot_version", "__rejected__$$$"]
67
67
  cleansed_header = tabular.cleanse_header!
68
68
  assert_equal(expected_header, cleansed_header)
69
69
  end
@@ -82,13 +82,13 @@ class TabularTest < Minitest::Test
82
82
  assert_equal @allowed_columns, tabular.header.allowed_columns
83
83
  end
84
84
 
85
- it "nils columns not in the whitelist" do
85
+ it "nils columns not in the allowed list" do
86
86
  tabular = IOStreams::Tabular.new(columns: [" first ", "Unknown Column", "thirD "], allowed_columns: @allowed_columns)
87
87
  header = tabular.cleanse_header!
88
- assert_equal ["first", nil, "third"], header
88
+ assert_equal ["first", "__rejected__Unknown Column", "third"], header
89
89
  end
90
90
 
91
- it "raises exception for columns not in the whitelist" do
91
+ it "raises exception for columns not in the allowed list" do
92
92
  tabular = IOStreams::Tabular.new(columns: [" first ", "Unknown Column", "thirD "], allowed_columns: @allowed_columns, skip_unknown: false)
93
93
  exc = assert_raises IOStreams::Errors::InvalidHeader do
94
94
  tabular.cleanse_header!
@@ -218,7 +218,7 @@ class TabularTest < Minitest::Test
218
218
  end
219
219
  end
220
220
 
221
- it "skips columns not in the whitelist" do
221
+ it "skips columns not in the allowed list" do
222
222
  tabular.header.allowed_columns = %w[first second third fourth fifth]
223
223
  tabular.cleanse_header!
224
224
  assert hash = tabular.record_parse("1,2,3")
data/test/test_helper.rb CHANGED
@@ -2,7 +2,6 @@ $LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib"
2
2
 
3
3
  require "yaml"
4
4
  require "minitest/autorun"
5
- require "minitest/reporters"
6
5
  require "iostreams"
7
6
  require "amazing_print"
8
7
  require "symmetric-encryption"
@@ -10,8 +9,6 @@ require "symmetric-encryption"
10
9
  # Since PGP libraries use UTC for Dates
11
10
  ENV["TZ"] = "UTC"
12
11
 
13
- Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
14
-
15
12
  # Test cipher
16
13
  SymmetricEncryption.cipher = SymmetricEncryption::Cipher.new(
17
14
  cipher_name: "aes-128-cbc",
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iostreams
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid Morrison
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-08 00:00:00.000000000 Z
11
+ date: 2021-07-22 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -125,14 +125,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
125
125
  requirements:
126
126
  - - ">="
127
127
  - !ruby/object:Gem::Version
128
- version: '2.3'
128
+ version: '2.5'
129
129
  required_rubygems_version: !ruby/object:Gem::Requirement
130
130
  requirements:
131
131
  - - ">="
132
132
  - !ruby/object:Gem::Version
133
133
  version: '0'
134
134
  requirements: []
135
- rubygems_version: 3.2.3
135
+ rubygems_version: 3.2.22
136
136
  signing_key:
137
137
  specification_version: 4
138
138
  summary: Input and Output streaming for Ruby.