iostreams 1.0.0.beta → 1.0.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/io_streams/encode/reader.rb +5 -2
- data/lib/io_streams/encode/writer.rb +2 -1
- data/lib/io_streams/io_streams.rb +7 -6
- data/lib/io_streams/line/reader.rb +2 -2
- data/lib/io_streams/line/writer.rb +3 -3
- data/lib/io_streams/path.rb +12 -11
- data/lib/io_streams/paths/http.rb +8 -3
- data/lib/io_streams/paths/s3.rb +16 -15
- data/lib/io_streams/paths/sftp.rb +11 -8
- data/lib/io_streams/record/reader.rb +2 -2
- data/lib/io_streams/record/writer.rb +3 -3
- data/lib/io_streams/row/reader.rb +2 -2
- data/lib/io_streams/row/writer.rb +1 -1
- data/lib/io_streams/stream.rb +20 -125
- data/lib/io_streams/version.rb +1 -1
- data/test/stream_test.rb +300 -6
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4469de3d596bf7fd0485de78878ad8188c0a60c8a3b52cfbd2dcd207ec4e8e92
|
4
|
+
data.tar.gz: 25e2e16fa02f1d8b964cd8bdb521576a106148fd6446ebddf832735e5006d403
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 37456322483004e740a678a46a32bae7dc1fd02d9ad1363a0ce02c95502cb0fb68d07295280193277b5b252b3ae39162a019ba3f85f1483c3e5503011ae68419
|
7
|
+
data.tar.gz: ea9cd84e99f50b7771d7429f78da254b15ffd1b08cb7cadfa4cb7c9033bb3cc6f59064addd3824e4bcf5f1072669268916c16cd2116d7e5b6e822bd34f6f4415
|
@@ -7,7 +7,9 @@ module IOStreams
|
|
7
7
|
# Builtin strip options to apply after encoding the read data.
|
8
8
|
CLEANSE_RULES = {
|
9
9
|
# Strips all non printable characters
|
10
|
-
printable: ->(data) { data.gsub!(NOT_PRINTABLE, '') || data }
|
10
|
+
printable: ->(data, _) { data.gsub!(NOT_PRINTABLE, '') || data },
|
11
|
+
# Replaces non printable characters with the value specified in the `replace` option.
|
12
|
+
replace_non_printable: ->(data, replace) { data.gsub!(NOT_PRINTABLE, replace || '') || data }
|
11
13
|
}.freeze
|
12
14
|
|
13
15
|
# Read a line at a time from a file or stream
|
@@ -46,6 +48,7 @@ module IOStreams
|
|
46
48
|
@cleaner = self.class.extract_cleaner(cleaner)
|
47
49
|
@encoding = encoding.nil? || encoding.is_a?(Encoding) ? encoding : Encoding.find(encoding)
|
48
50
|
@encoding_options = replace.nil? ? {} : {invalid: :replace, undef: :replace, replace: replace}
|
51
|
+
@replace = replace
|
49
52
|
|
50
53
|
# More efficient read buffering only supported when the input stream `#read` method supports it.
|
51
54
|
if replace.nil? && !@input_stream.method(:read).arity.between?(0, 1)
|
@@ -75,7 +78,7 @@ module IOStreams
|
|
75
78
|
return unless block
|
76
79
|
|
77
80
|
block = block.encode(@encoding, @encoding_options) unless block.encoding == @encoding
|
78
|
-
block = @cleaner.call(block) if @cleaner
|
81
|
+
block = @cleaner.call(block, @replace) if @cleaner
|
79
82
|
block
|
80
83
|
end
|
81
84
|
|
@@ -40,6 +40,7 @@ module IOStreams
|
|
40
40
|
@cleaner = ::IOStreams::Encode::Reader.send(:extract_cleaner, cleaner)
|
41
41
|
@encoding = encoding.nil? || encoding.is_a?(Encoding) ? encoding : Encoding.find(encoding)
|
42
42
|
@encoding_options = replace.nil? ? {} : {invalid: :replace, undef: :replace, replace: replace}
|
43
|
+
@replace = replace
|
43
44
|
end
|
44
45
|
|
45
46
|
# Write a line to the output stream
|
@@ -66,7 +67,7 @@ module IOStreams
|
|
66
67
|
|
67
68
|
data = data.to_s
|
68
69
|
block = data.encoding == @encoding ? data : data.encode(@encoding, @encoding_options)
|
69
|
-
block = @cleaner.call(block) if @cleaner
|
70
|
+
block = @cleaner.call(block, @replace) if @cleaner
|
70
71
|
@output_stream.write(block)
|
71
72
|
end
|
72
73
|
end
|
@@ -47,13 +47,14 @@ module IOStreams
|
|
47
47
|
# IOStreams.path('blah.zip').stream(:zip).stream(:pgp, passphrase: 'receiver_passphrase').reader(&:read)
|
48
48
|
# IOStreams.path('blah.zip').stream(:zip).stream(:encode, encoding: 'BINARY').reader(&:read)
|
49
49
|
#
|
50
|
-
def self.path(*elements)
|
51
|
-
return elements.first if (elements.size == 1) && elements.first.is_a?(IOStreams::Path)
|
50
|
+
def self.path(*elements, **args)
|
51
|
+
return elements.first if (elements.size == 1) && args.empty? && elements.first.is_a?(IOStreams::Path)
|
52
52
|
|
53
|
-
elements
|
54
|
-
path
|
55
|
-
|
56
|
-
scheme(
|
53
|
+
elements = elements.collect(&:to_s)
|
54
|
+
path = ::File.join(*elements)
|
55
|
+
extracted_scheme = path.include?("://") ? URI.parse(path).scheme : nil
|
56
|
+
klass = scheme(extracted_scheme)
|
57
|
+
args.empty? ? klass.new(path) : klass.new(path, **args)
|
57
58
|
end
|
58
59
|
|
59
60
|
# For an existing IO Stream
|
@@ -9,9 +9,9 @@ module IOStreams
|
|
9
9
|
LINEFEED_REGEXP = Regexp.compile(/\r\n|\n|\r/).freeze
|
10
10
|
|
11
11
|
# Read a line at a time from a stream
|
12
|
-
def self.stream(input_stream, original_file_name: nil, **args
|
12
|
+
def self.stream(input_stream, original_file_name: nil, **args)
|
13
13
|
# Pass-through if already a line reader
|
14
|
-
return
|
14
|
+
return yield(input_stream) if input_stream.is_a?(self.class)
|
15
15
|
|
16
16
|
yield new(input_stream, **args)
|
17
17
|
end
|
@@ -4,9 +4,9 @@ module IOStreams
|
|
4
4
|
attr_reader :delimiter
|
5
5
|
|
6
6
|
# Write a line at a time to a stream.
|
7
|
-
def self.stream(output_stream,
|
7
|
+
def self.stream(output_stream, **args)
|
8
8
|
# Pass-through if already a line writer
|
9
|
-
return
|
9
|
+
return yield(output_stream) if output_stream.is_a?(self.class)
|
10
10
|
|
11
11
|
yield new(output_stream, **args)
|
12
12
|
end
|
@@ -24,7 +24,7 @@ module IOStreams
|
|
24
24
|
# Add the specified delimiter after every record when writing it
|
25
25
|
# to the output stream
|
26
26
|
# Default: OS Specific. Linux: "\n"
|
27
|
-
def initialize(output_stream, delimiter:
|
27
|
+
def initialize(output_stream, delimiter: $/, original_file_name: nil)
|
28
28
|
super(output_stream)
|
29
29
|
@delimiter = delimiter
|
30
30
|
end
|
data/lib/io_streams/path.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module IOStreams
|
2
2
|
class Path < IOStreams::Stream
|
3
|
-
|
3
|
+
attr_accessor :path
|
4
4
|
|
5
5
|
def initialize(path)
|
6
6
|
raise(ArgumentError, 'Path cannot be nil') if path.nil?
|
@@ -18,11 +18,11 @@ module IOStreams
|
|
18
18
|
|
19
19
|
elements = elements.collect(&:to_s)
|
20
20
|
relative = ::File.join(*elements)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
|
22
|
+
new_path = dup
|
23
|
+
new_path.streams = nil
|
24
|
+
new_path.path = relative.start_with?(path) ? relative : ::File.join(path, relative)
|
25
|
+
new_path
|
26
26
|
end
|
27
27
|
|
28
28
|
def relative?
|
@@ -103,7 +103,6 @@ module IOStreams
|
|
103
103
|
end
|
104
104
|
|
105
105
|
# Returns [IOStreams::Path] the directory for this file.
|
106
|
-
# Returns `nil` if no `file_name` was set.
|
107
106
|
#
|
108
107
|
# If `path` does not include a directory name then "." is returned.
|
109
108
|
#
|
@@ -114,8 +113,10 @@ module IOStreams
|
|
114
113
|
# IOStreams.path("test").directory #=> "."
|
115
114
|
# IOStreams.path(".profile").directory #=> "."
|
116
115
|
def directory
|
117
|
-
|
118
|
-
|
116
|
+
new_path = dup
|
117
|
+
new_path.streams = nil
|
118
|
+
new_path.path = ::File.dirname(path)
|
119
|
+
new_path
|
119
120
|
end
|
120
121
|
|
121
122
|
# When path is a file, deletes this file.
|
@@ -166,12 +167,12 @@ module IOStreams
|
|
166
167
|
|
167
168
|
# Paths are sortable by name
|
168
169
|
def <=>(other)
|
169
|
-
path <=> other.
|
170
|
+
path <=> other.path
|
170
171
|
end
|
171
172
|
|
172
173
|
# Compare by path name, ignore streams
|
173
174
|
def ==(other)
|
174
|
-
path == other.
|
175
|
+
path == other.path
|
175
176
|
end
|
176
177
|
|
177
178
|
def inspect
|
@@ -3,7 +3,7 @@ require 'uri'
|
|
3
3
|
module IOStreams
|
4
4
|
module Paths
|
5
5
|
class HTTP < IOStreams::Path
|
6
|
-
attr_reader :username, :password, :http_redirect_count
|
6
|
+
attr_reader :username, :password, :http_redirect_count, :url
|
7
7
|
|
8
8
|
# Stream to/from a remote file over http(s).
|
9
9
|
#
|
@@ -34,7 +34,12 @@ module IOStreams
|
|
34
34
|
@username = username || uri.user
|
35
35
|
@password = password || uri.password
|
36
36
|
@http_redirect_count = http_redirect_count
|
37
|
-
|
37
|
+
@url = url
|
38
|
+
super(uri.path)
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s
|
42
|
+
url
|
38
43
|
end
|
39
44
|
|
40
45
|
# Read a file using an http get.
|
@@ -48,7 +53,7 @@ module IOStreams
|
|
48
53
|
# Notes:
|
49
54
|
# * Since Net::HTTP download only supports a push stream, the data is streamed into a tempfile first.
|
50
55
|
def reader(&block)
|
51
|
-
handle_redirects(
|
56
|
+
handle_redirects(url, http_redirect_count, &block)
|
52
57
|
end
|
53
58
|
|
54
59
|
def handle_redirects(uri, http_redirect_count, &block)
|
data/lib/io_streams/paths/s3.rb
CHANGED
@@ -3,14 +3,14 @@ require "uri"
|
|
3
3
|
module IOStreams
|
4
4
|
module Paths
|
5
5
|
class S3 < IOStreams::Path
|
6
|
-
attr_reader :bucket_name, :
|
6
|
+
attr_reader :bucket_name, :client
|
7
7
|
|
8
8
|
# Arguments:
|
9
9
|
#
|
10
10
|
# url: [String]
|
11
11
|
# Prefix must be: `s3://`
|
12
12
|
# followed by bucket name,
|
13
|
-
# followed by
|
13
|
+
# followed by key.
|
14
14
|
# Examples:
|
15
15
|
# s3://my-bucket-name/file_name.txt
|
16
16
|
# s3://my-bucket-name/some_path/file_name.csv
|
@@ -62,9 +62,6 @@ module IOStreams
|
|
62
62
|
# @option params [String] :grant_write_acp
|
63
63
|
# Allows grantee to write the ACL for the applicable object.
|
64
64
|
#
|
65
|
-
# @option params [required, String] :key
|
66
|
-
# Object key for which the PUT operation was initiated.
|
67
|
-
#
|
68
65
|
# @option params [Hash<String,String>] :metadata
|
69
66
|
# A map of metadata to store with the object in S3.
|
70
67
|
#
|
@@ -134,21 +131,25 @@ module IOStreams
|
|
134
131
|
raise "Invalid URI. Required Format: 's3://<bucket_name>/<key>'" unless uri.scheme == 's3'
|
135
132
|
|
136
133
|
@bucket_name = uri.host
|
137
|
-
|
134
|
+
key = uri.path.sub(%r{\A/}, '')
|
138
135
|
@client = client || ::Aws::S3::Client.new
|
139
136
|
@options = args
|
140
|
-
super(
|
137
|
+
super(key)
|
138
|
+
end
|
139
|
+
|
140
|
+
def to_s
|
141
|
+
::File.join("s3://", bucket_name, path)
|
141
142
|
end
|
142
143
|
|
143
144
|
def delete
|
144
|
-
client.delete_object(bucket: bucket_name, key:
|
145
|
+
client.delete_object(bucket: bucket_name, key: path)
|
145
146
|
self
|
146
147
|
rescue Aws::S3::Errors::NotFound
|
147
148
|
self
|
148
149
|
end
|
149
150
|
|
150
151
|
def exist?
|
151
|
-
client.head_object(bucket: bucket_name, key:
|
152
|
+
client.head_object(bucket: bucket_name, key: path)
|
152
153
|
true
|
153
154
|
rescue Aws::S3::Errors::NotFound
|
154
155
|
false
|
@@ -162,9 +163,9 @@ module IOStreams
|
|
162
163
|
target = IOStreams.new(target_path)
|
163
164
|
return super(target) unless target.is_a?(self.class)
|
164
165
|
|
165
|
-
source_name = ::File.join(bucket_name,
|
166
|
+
source_name = ::File.join(bucket_name, path)
|
166
167
|
# TODO: Does/should it also copy metadata?
|
167
|
-
client.copy_object(bucket: target.bucket_name, key: target.
|
168
|
+
client.copy_object(bucket: target.bucket_name, key: target.path, copy_source: source_name)
|
168
169
|
delete
|
169
170
|
target
|
170
171
|
end
|
@@ -179,7 +180,7 @@ module IOStreams
|
|
179
180
|
end
|
180
181
|
|
181
182
|
def size
|
182
|
-
client.head_object(bucket: bucket_name, key:
|
183
|
+
client.head_object(bucket: bucket_name, key: path).content_length
|
183
184
|
rescue Aws::S3::Errors::NotFound
|
184
185
|
nil
|
185
186
|
end
|
@@ -199,7 +200,7 @@ module IOStreams
|
|
199
200
|
# Shortcut method if caller has a filename already with no other streams applied:
|
200
201
|
def read_file(file_name)
|
201
202
|
::File.open(file_name, 'wb') do |file|
|
202
|
-
client.get_object(@options.merge(response_target: file, bucket: bucket_name, key:
|
203
|
+
client.get_object(@options.merge(response_target: file, bucket: bucket_name, key: path))
|
203
204
|
end
|
204
205
|
end
|
205
206
|
|
@@ -226,11 +227,11 @@ module IOStreams
|
|
226
227
|
if ::File.size(file_name) > 5 * 1024 * 1024
|
227
228
|
# Use multipart file upload
|
228
229
|
s3 = Aws::S3::Resource.new(client: client)
|
229
|
-
obj = s3.bucket(bucket_name).object(
|
230
|
+
obj = s3.bucket(bucket_name).object(path)
|
230
231
|
obj.upload_file(file_name)
|
231
232
|
else
|
232
233
|
::File.open(file_name, 'rb') do |file|
|
233
|
-
client.put_object(@options.merge(bucket: bucket_name, key:
|
234
|
+
client.put_object(@options.merge(bucket: bucket_name, key: path, body: file))
|
234
235
|
end
|
235
236
|
end
|
236
237
|
end
|
@@ -3,12 +3,12 @@ module IOStreams
|
|
3
3
|
class SFTP < IOStreams::Path
|
4
4
|
include SemanticLogger::Loggable if defined?(SemanticLogger)
|
5
5
|
|
6
|
-
attr_reader :hostname, :username, :
|
6
|
+
attr_reader :hostname, :username, :create_path, :options, :url
|
7
7
|
|
8
8
|
# Stream to a remote file over sftp.
|
9
9
|
#
|
10
|
-
#
|
11
|
-
#
|
10
|
+
# url: [String]
|
11
|
+
# "sftp://<host_name>/<file_name>"
|
12
12
|
#
|
13
13
|
# username: [String]
|
14
14
|
# Name of user to login with.
|
@@ -39,7 +39,6 @@ module IOStreams
|
|
39
39
|
raise(ArgumentError, "Invalid URL. Required Format: 'sftp://<host_name>/<file_name>'") unless uri.scheme == 'sftp'
|
40
40
|
|
41
41
|
@hostname = uri.hostname
|
42
|
-
@file_name = uri.path
|
43
42
|
@mkdir = false
|
44
43
|
@username = username || uri.user
|
45
44
|
@create_path = create_path
|
@@ -51,7 +50,11 @@ module IOStreams
|
|
51
50
|
options[:max_pkt_size] = max_pkt_size
|
52
51
|
options[:password] = password || uri.password
|
53
52
|
@options = options
|
54
|
-
super(
|
53
|
+
super(uri.path)
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s
|
57
|
+
url
|
55
58
|
end
|
56
59
|
|
57
60
|
def mkdir
|
@@ -73,7 +76,7 @@ module IOStreams
|
|
73
76
|
def reader(&block)
|
74
77
|
result = nil
|
75
78
|
Net::SFTP.start(hostname, username, options) do |sftp|
|
76
|
-
result = sftp.file.open(
|
79
|
+
result = sftp.file.open(path, 'rb', &block)
|
77
80
|
end
|
78
81
|
result
|
79
82
|
end
|
@@ -89,8 +92,8 @@ module IOStreams
|
|
89
92
|
def writer(&block)
|
90
93
|
result = nil
|
91
94
|
Net::SFTP.start(hostname, username, options) do |sftp|
|
92
|
-
sftp.session.exec!("mkdir -p '#{::File.dirname(
|
93
|
-
result = sftp.file.open(
|
95
|
+
sftp.session.exec!("mkdir -p '#{::File.dirname(path)}'") if create_path
|
96
|
+
result = sftp.file.open(path, 'wb', &block)
|
94
97
|
end
|
95
98
|
result
|
96
99
|
end
|
@@ -7,9 +7,9 @@ module IOStreams
|
|
7
7
|
# Read a record at a time from a line stream
|
8
8
|
# Note:
|
9
9
|
# - The supplied stream _must_ already be a line stream, or a stream that responds to :each
|
10
|
-
def self.stream(line_reader, original_file_name: nil, **args
|
10
|
+
def self.stream(line_reader, original_file_name: nil, **args)
|
11
11
|
# Pass-through if already a record reader
|
12
|
-
return
|
12
|
+
return yield(line_reader) if line_reader.is_a?(self.class)
|
13
13
|
|
14
14
|
yield new(line_reader, **args)
|
15
15
|
end
|
@@ -9,9 +9,9 @@ module IOStreams
|
|
9
9
|
# Write a record as a Hash at a time to a stream.
|
10
10
|
# Note:
|
11
11
|
# - The supplied stream _must_ already be a line stream, or a stream that responds to :<<
|
12
|
-
def self.stream(line_writer, original_file_name: nil, **args
|
13
|
-
# Pass-through if already a
|
14
|
-
return
|
12
|
+
def self.stream(line_writer, original_file_name: nil, **args)
|
13
|
+
# Pass-through if already a record writer
|
14
|
+
return yield(line_writer) if line_writer.is_a?(self.class)
|
15
15
|
|
16
16
|
yield new(line_writer, **args)
|
17
17
|
end
|
@@ -5,9 +5,9 @@ module IOStreams
|
|
5
5
|
# Read a line as an Array at a time from a stream.
|
6
6
|
# Note:
|
7
7
|
# - The supplied stream _must_ already be a line stream, or a stream that responds to :each
|
8
|
-
def self.stream(line_reader, original_file_name: nil, **args
|
8
|
+
def self.stream(line_reader, original_file_name: nil, **args)
|
9
9
|
# Pass-through if already a row reader
|
10
|
-
return
|
10
|
+
return yield(line_reader) if line_reader.is_a?(self.class)
|
11
11
|
|
12
12
|
yield new(line_reader, **args)
|
13
13
|
end
|
@@ -14,7 +14,7 @@ module IOStreams
|
|
14
14
|
# - The supplied stream _must_ already be a line stream, or a stream that responds to :<<
|
15
15
|
def self.stream(line_writer, original_file_name: nil, **args)
|
16
16
|
# Pass-through if already a row writer
|
17
|
-
return
|
17
|
+
return yield(line_writer) if line_writer.is_a?(self.class)
|
18
18
|
|
19
19
|
yield new(line_writer, **args)
|
20
20
|
end
|
data/lib/io_streams/stream.rb
CHANGED
@@ -58,50 +58,6 @@ module IOStreams
|
|
58
58
|
end
|
59
59
|
|
60
60
|
# Returns a Reader for reading a file / stream
|
61
|
-
#
|
62
|
-
# Parameters
|
63
|
-
# file_name_or_io [String|IO]
|
64
|
-
# The file_name of the file to write to, or an IO Stream that implements
|
65
|
-
# #read.
|
66
|
-
#
|
67
|
-
# streams [Symbol|Array]
|
68
|
-
# The formats/streams that be used to convert the data whilst it is
|
69
|
-
# being read.
|
70
|
-
# When nil, the file_name will be inspected to try and determine what
|
71
|
-
# streams should be applied.
|
72
|
-
# Default: nil
|
73
|
-
#
|
74
|
-
# file_name [String]
|
75
|
-
# When `streams` is not supplied, `file_name` can be used for determining the streams
|
76
|
-
# to apply to read the file/stream.
|
77
|
-
# This is particularly useful when `file_name_or_io` is a stream, or a temporary file name.
|
78
|
-
# Default: nil
|
79
|
-
#
|
80
|
-
# Example: Zip
|
81
|
-
# IOStreams.reader('myfile.zip') do |stream|
|
82
|
-
# puts stream.read
|
83
|
-
# end
|
84
|
-
#
|
85
|
-
# Example: Encrypted Zip
|
86
|
-
# IOStreams.reader('myfile.zip.enc') do |stream|
|
87
|
-
# puts stream.read
|
88
|
-
# end
|
89
|
-
#
|
90
|
-
# Example: Explicitly set the streams
|
91
|
-
# IOStreams.reader('myfile.zip.enc', [:zip, :enc]) do |stream|
|
92
|
-
# puts stream.read
|
93
|
-
# end
|
94
|
-
#
|
95
|
-
# Example: Supply custom options
|
96
|
-
# # Encrypt the file and get Symmetric Encryption to also compress it
|
97
|
-
# IOStreams.reader('myfile.csv.enc', streams: enc: {compress: true}) do |stream|
|
98
|
-
# puts stream.read
|
99
|
-
# end
|
100
|
-
#
|
101
|
-
# Note:
|
102
|
-
# * Passes the file_name_or_io as-is into the block if it is already a reader stream AND
|
103
|
-
# no streams are passed in.
|
104
|
-
#
|
105
61
|
def reader(&block)
|
106
62
|
streams.reader(io_stream, &block)
|
107
63
|
end
|
@@ -141,12 +97,12 @@ module IOStreams
|
|
141
97
|
def copy_from(source, convert: true)
|
142
98
|
if convert
|
143
99
|
stream = IOStreams.new(source)
|
144
|
-
|
100
|
+
writer do |target|
|
145
101
|
stream.reader { |src| IO.copy_stream(src, target) }
|
146
102
|
end
|
147
103
|
else
|
148
104
|
stream = source.is_a?(Stream) ? source.dup : IOStreams.new(source)
|
149
|
-
|
105
|
+
dup.stream(:none).writer do |target|
|
150
106
|
stream.stream(:none).reader { |src| IO.copy_stream(src, target) }
|
151
107
|
end
|
152
108
|
end
|
@@ -182,26 +138,6 @@ module IOStreams
|
|
182
138
|
end
|
183
139
|
|
184
140
|
# Returns [Hash] of every record in a file or stream with support for headers.
|
185
|
-
#
|
186
|
-
# Reading a delimited stream and converting to tabular form.
|
187
|
-
#
|
188
|
-
# Each record / line is returned one at a time so that very large files
|
189
|
-
# can be read without having to load the entire file into memory.
|
190
|
-
#
|
191
|
-
# Embedded lines (within double quotes) will be skipped if
|
192
|
-
# 1. The file name contains .csv
|
193
|
-
# 2. Or the embedded_within argument is set
|
194
|
-
#
|
195
|
-
# Example: Supply custom options
|
196
|
-
# IOStreams.each_record(file_name, embedded_within: '"') do |line|
|
197
|
-
# puts line
|
198
|
-
# end
|
199
|
-
#
|
200
|
-
# Example:
|
201
|
-
# file_name = 'customer_data.csv.pgp'
|
202
|
-
# IOStreams.each_record(file_name) do |hash|
|
203
|
-
# p hash
|
204
|
-
# end
|
205
141
|
def each_record(**args, &block)
|
206
142
|
record_reader(**args) { |record_stream| record_stream.each(&block) }
|
207
143
|
end
|
@@ -229,57 +165,6 @@ module IOStreams
|
|
229
165
|
end
|
230
166
|
|
231
167
|
# Returns a Writer for writing to a file / stream
|
232
|
-
#
|
233
|
-
# Parameters
|
234
|
-
# file_name_or_io [String|IO]
|
235
|
-
# The file_name of the file to write to, or an IO Stream that implements
|
236
|
-
# #write.
|
237
|
-
#
|
238
|
-
# streams [Symbol|Array]
|
239
|
-
# The formats/streams that be used to convert the data whilst it is
|
240
|
-
# being written.
|
241
|
-
# When nil, the file_name will be inspected to try and determine what
|
242
|
-
# streams should be applied.
|
243
|
-
# Default: nil
|
244
|
-
#
|
245
|
-
# Stream types / extensions supported:
|
246
|
-
# .zip Zip File [ :zip ]
|
247
|
-
# .gz, .gzip GZip File [ :gzip ]
|
248
|
-
# .enc File Encrypted using symmetric encryption [ :enc ]
|
249
|
-
# other All other extensions will be returned as: [ :file ]
|
250
|
-
#
|
251
|
-
# When a file is encrypted, it may also be compressed:
|
252
|
-
# .zip.enc [ :zip, :enc ]
|
253
|
-
# .gz.enc [ :gz, :enc ]
|
254
|
-
#
|
255
|
-
# Example: Zip
|
256
|
-
# IOStreams.writer('myfile.zip') do |stream|
|
257
|
-
# stream.write(data)
|
258
|
-
# end
|
259
|
-
#
|
260
|
-
# Example: Encrypted Zip
|
261
|
-
# IOStreams.writer('myfile.zip.enc') do |stream|
|
262
|
-
# stream.write(data)
|
263
|
-
# end
|
264
|
-
#
|
265
|
-
# Example: Explicitly set the streams
|
266
|
-
# IOStreams.writer('myfile.zip.enc', [:zip, :enc]) do |stream|
|
267
|
-
# stream.write(data)
|
268
|
-
# end
|
269
|
-
#
|
270
|
-
# Example: Supply custom options
|
271
|
-
# IOStreams.writer('myfile.csv.enc', [enc: { compress: true }]) do |stream|
|
272
|
-
# stream.write(data)
|
273
|
-
# end
|
274
|
-
#
|
275
|
-
# Example: Set internal filename when creating a zip file
|
276
|
-
# IOStreams.writer('myfile.csv.zip', zip: { zip_file_name: 'myfile.csv' }) do |stream|
|
277
|
-
# stream.write(data)
|
278
|
-
# end
|
279
|
-
#
|
280
|
-
# Note:
|
281
|
-
# * Passes the file_name_or_io as-is into the block if it is already a writer stream AND
|
282
|
-
# no streams are passed in.
|
283
168
|
def writer(&block)
|
284
169
|
streams.writer(io_stream, &block)
|
285
170
|
end
|
@@ -295,22 +180,32 @@ module IOStreams
|
|
295
180
|
writer { |stream| stream.write(data) }
|
296
181
|
end
|
297
182
|
|
298
|
-
def line_writer(**args)
|
299
|
-
|
183
|
+
def line_writer(**args, &block)
|
184
|
+
return block.call(io_stream) if io_stream && io_stream.is_a?(IOStreams::Line::Writer)
|
185
|
+
|
186
|
+
writer { |io| IOStreams::Line::Writer.stream(io, **args, &block) }
|
300
187
|
end
|
301
188
|
|
302
|
-
def row_writer(delimiter: $/, **args)
|
303
|
-
|
189
|
+
def row_writer(delimiter: $/, **args, &block)
|
190
|
+
return block.call(io_stream) if io_stream && io_stream.is_a?(IOStreams::Row::Writer)
|
191
|
+
|
192
|
+
line_writer(delimiter: delimiter) { |io| IOStreams::Row::Writer.stream(io, **args, &block) }
|
304
193
|
end
|
305
194
|
|
306
|
-
def record_writer(delimiter: $/, **args)
|
307
|
-
|
195
|
+
def record_writer(delimiter: $/, **args, &block)
|
196
|
+
return block.call(io_stream) if io_stream && io_stream.is_a?(IOStreams::Record::Writer)
|
197
|
+
|
198
|
+
line_writer(delimiter: delimiter) { |io| IOStreams::Record::Writer.stream(io, **args, &block) }
|
308
199
|
end
|
309
200
|
|
310
201
|
# Set/get the original file_name
|
311
202
|
def file_name(file_name = :none)
|
312
|
-
file_name == :none
|
313
|
-
|
203
|
+
if file_name == :none
|
204
|
+
streams.file_name
|
205
|
+
else
|
206
|
+
streams.file_name = file_name
|
207
|
+
self
|
208
|
+
end
|
314
209
|
end
|
315
210
|
|
316
211
|
# Set/get the original file_name
|
data/lib/io_streams/version.rb
CHANGED
data/test/stream_test.rb
CHANGED
@@ -97,19 +97,313 @@ class StreamTest < Minitest::Test
|
|
97
97
|
describe '.each_record' do
|
98
98
|
end
|
99
99
|
|
100
|
-
describe '
|
101
|
-
|
100
|
+
describe '#writer' do
|
101
|
+
describe "#write" do
|
102
|
+
it 'one block' do
|
103
|
+
io = StringIO.new
|
104
|
+
IOStreams::Stream.new(io).writer do |stream|
|
105
|
+
stream.write("Hello World")
|
106
|
+
end
|
107
|
+
assert_equal "Hello World", io.string
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'multiple blocks' do
|
111
|
+
io = StringIO.new
|
112
|
+
IOStreams::Stream.new(io).writer do |stream|
|
113
|
+
stream.write("He")
|
114
|
+
stream.write("l")
|
115
|
+
stream.write("lo ")
|
116
|
+
stream.write("World")
|
117
|
+
end
|
118
|
+
assert_equal "Hello World", io.string
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'empty blocks' do
|
122
|
+
io = StringIO.new
|
123
|
+
IOStreams::Stream.new(io).writer do |stream|
|
124
|
+
stream.write("")
|
125
|
+
stream.write("He")
|
126
|
+
stream.write("")
|
127
|
+
stream.write("l")
|
128
|
+
stream.write("")
|
129
|
+
stream.write("lo ")
|
130
|
+
stream.write("World")
|
131
|
+
stream.write("")
|
132
|
+
end
|
133
|
+
assert_equal "Hello World", io.string
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'nil blocks' do
|
137
|
+
io = StringIO.new
|
138
|
+
IOStreams::Stream.new(io).writer do |stream|
|
139
|
+
stream.write(nil)
|
140
|
+
stream.write("He")
|
141
|
+
stream.write(nil)
|
142
|
+
stream.write("l")
|
143
|
+
stream.write(nil)
|
144
|
+
stream.write("lo ")
|
145
|
+
stream.write("World")
|
146
|
+
stream.write(nil)
|
147
|
+
end
|
148
|
+
assert_equal "Hello World", io.string
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
describe "#<<" do
|
153
|
+
it 'one block' do
|
154
|
+
io = StringIO.new
|
155
|
+
IOStreams::Stream.new(io).writer do |stream|
|
156
|
+
stream << "Hello World"
|
157
|
+
end
|
158
|
+
assert_equal "Hello World", io.string
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'multiple blocks' do
|
162
|
+
io = StringIO.new
|
163
|
+
IOStreams::Stream.new(io).writer do |stream|
|
164
|
+
stream << "He"
|
165
|
+
stream << "l" << "lo " << "World"
|
166
|
+
end
|
167
|
+
assert_equal "Hello World", io.string
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'empty blocks' do
|
171
|
+
io = StringIO.new
|
172
|
+
IOStreams::Stream.new(io).writer do |stream|
|
173
|
+
stream << ""
|
174
|
+
stream << "He" << "" << "l" << ""
|
175
|
+
stream << "lo " << "World"
|
176
|
+
stream << ""
|
177
|
+
end
|
178
|
+
assert_equal "Hello World", io.string
|
179
|
+
end
|
102
180
|
|
103
|
-
|
181
|
+
it 'nil blocks' do
|
182
|
+
io = StringIO.new
|
183
|
+
IOStreams::Stream.new(io).writer do |stream|
|
184
|
+
stream << nil
|
185
|
+
stream << "He" << nil << "l" << nil
|
186
|
+
stream << "lo " << "World"
|
187
|
+
stream << nil
|
188
|
+
end
|
189
|
+
assert_equal "Hello World", io.string
|
190
|
+
end
|
191
|
+
end
|
104
192
|
end
|
105
193
|
|
106
|
-
describe '
|
194
|
+
describe '#line_writer' do
|
195
|
+
describe "#write" do
|
196
|
+
it 'one block' do
|
197
|
+
io = StringIO.new
|
198
|
+
IOStreams::Stream.new(io).line_writer do |stream|
|
199
|
+
stream.write("Hello World")
|
200
|
+
end
|
201
|
+
assert_equal "Hello World\n", io.string
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'multiple blocks' do
|
205
|
+
io = StringIO.new
|
206
|
+
IOStreams::Stream.new(io).line_writer do |stream|
|
207
|
+
stream.write("He")
|
208
|
+
stream.write("l")
|
209
|
+
stream.write("lo ")
|
210
|
+
stream.write("World")
|
211
|
+
end
|
212
|
+
assert_equal "He\nl\nlo \nWorld\n", io.string
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'empty blocks' do
|
216
|
+
io = StringIO.new
|
217
|
+
IOStreams::Stream.new(io).line_writer do |stream|
|
218
|
+
stream.write("")
|
219
|
+
stream.write("He")
|
220
|
+
stream.write("")
|
221
|
+
stream.write("l")
|
222
|
+
stream.write("")
|
223
|
+
stream.write("lo ")
|
224
|
+
stream.write("World")
|
225
|
+
stream.write("")
|
226
|
+
end
|
227
|
+
assert_equal "\nHe\n\nl\n\nlo \nWorld\n\n", io.string, io.string.inspect
|
228
|
+
end
|
229
|
+
|
230
|
+
it 'nil blocks' do
|
231
|
+
io = StringIO.new
|
232
|
+
IOStreams::Stream.new(io).line_writer do |stream|
|
233
|
+
stream.write(nil)
|
234
|
+
stream.write("He")
|
235
|
+
stream.write(nil)
|
236
|
+
stream.write("l")
|
237
|
+
stream.write(nil)
|
238
|
+
stream.write("lo ")
|
239
|
+
stream.write("World")
|
240
|
+
stream.write(nil)
|
241
|
+
end
|
242
|
+
assert_equal "\nHe\n\nl\n\nlo \nWorld\n\n", io.string, io.string.inspect
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
describe "#<<" do
|
247
|
+
it 'one block' do
|
248
|
+
io = StringIO.new
|
249
|
+
IOStreams::Stream.new(io).line_writer do |stream|
|
250
|
+
stream << "Hello World"
|
251
|
+
end
|
252
|
+
assert_equal "Hello World\n", io.string
|
253
|
+
end
|
254
|
+
|
255
|
+
it 'multiple blocks' do
|
256
|
+
io = StringIO.new
|
257
|
+
IOStreams::Stream.new(io).line_writer do |stream|
|
258
|
+
stream << "He"
|
259
|
+
stream << "l" << "lo " << "World"
|
260
|
+
end
|
261
|
+
assert_equal "He\nl\nlo \nWorld\n", io.string
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'empty blocks' do
|
265
|
+
io = StringIO.new
|
266
|
+
IOStreams::Stream.new(io).line_writer do |stream|
|
267
|
+
stream << ""
|
268
|
+
stream << "He" << "" << "l" << ""
|
269
|
+
stream << "lo " << "World"
|
270
|
+
stream << ""
|
271
|
+
end
|
272
|
+
assert_equal "\nHe\n\nl\n\nlo \nWorld\n\n", io.string
|
273
|
+
end
|
274
|
+
|
275
|
+
it 'nil blocks' do
|
276
|
+
io = StringIO.new
|
277
|
+
IOStreams::Stream.new(io).line_writer do |stream|
|
278
|
+
stream << nil
|
279
|
+
stream << "He" << nil << "l" << nil
|
280
|
+
stream << "lo " << "World"
|
281
|
+
stream << nil
|
282
|
+
end
|
283
|
+
assert_equal "\nHe\n\nl\n\nlo \nWorld\n\n", io.string
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
describe "line writers within line writers" do
|
288
|
+
it 'uses existing line writer' do
|
289
|
+
io = StringIO.new
|
290
|
+
IOStreams::Stream.new(io).line_writer do |stream|
|
291
|
+
stream.write("Before")
|
292
|
+
IOStreams::Stream.new(stream).line_writer do |inner|
|
293
|
+
stream.write("Inner")
|
294
|
+
assert_equal inner.object_id, stream.object_id
|
295
|
+
end
|
296
|
+
stream.write("After")
|
297
|
+
end
|
298
|
+
assert_equal "Before\nInner\nAfter\n", io.string, io.string.inspect
|
299
|
+
end
|
300
|
+
end
|
107
301
|
end
|
108
302
|
|
109
|
-
describe '
|
303
|
+
describe '#row_writer' do
|
304
|
+
describe "#write" do
|
305
|
+
it 'one block' do
|
306
|
+
io = StringIO.new
|
307
|
+
IOStreams::Stream.new(io).row_writer do |stream|
|
308
|
+
stream << %w[Hello World]
|
309
|
+
end
|
310
|
+
assert_equal "Hello,World\n", io.string
|
311
|
+
end
|
312
|
+
|
313
|
+
it 'multiple blocks' do
|
314
|
+
io = StringIO.new
|
315
|
+
IOStreams::Stream.new(io).row_writer do |stream|
|
316
|
+
stream << %w[He]
|
317
|
+
stream << %w[l lo\ World]
|
318
|
+
stream << ["He", "", "l", ""]
|
319
|
+
stream << ["lo ", "World"]
|
320
|
+
end
|
321
|
+
assert_equal "He\nl,lo ,World\nHe,\"\",l,\"\"\nlo ,World\n", io.string, io.string.inspect
|
322
|
+
end
|
323
|
+
|
324
|
+
it 'empty blocks' do
|
325
|
+
# skip "TODO"
|
326
|
+
io = StringIO.new
|
327
|
+
IOStreams::Stream.new(io).row_writer do |stream|
|
328
|
+
stream << %w[He]
|
329
|
+
stream << []
|
330
|
+
stream << %w[l lo\ World]
|
331
|
+
stream << ["He", "", "l", ""]
|
332
|
+
stream << ["lo ", "World"]
|
333
|
+
stream << []
|
334
|
+
end
|
335
|
+
assert_equal "He\n\nl,lo ,World\nHe,\"\",l,\"\"\nlo ,World\n\n", io.string, io.string.inspect
|
336
|
+
end
|
337
|
+
|
338
|
+
it 'nil values' do
|
339
|
+
io = StringIO.new
|
340
|
+
IOStreams::Stream.new(io).row_writer do |stream|
|
341
|
+
stream << %w[He]
|
342
|
+
stream << %w[l lo\ World]
|
343
|
+
stream << ["He", nil, "l", nil]
|
344
|
+
stream << ["lo ", "World"]
|
345
|
+
end
|
346
|
+
assert_equal "He\nl,lo ,World\nHe,,l,\nlo ,World\n", io.string, io.string.inspect
|
347
|
+
end
|
348
|
+
|
349
|
+
it 'empty leading array' do
|
350
|
+
skip "TODO"
|
351
|
+
io = StringIO.new
|
352
|
+
IOStreams::Stream.new(io).row_writer do |stream|
|
353
|
+
stream << []
|
354
|
+
stream << %w[He]
|
355
|
+
stream << %w[l lo\ World]
|
356
|
+
stream << ["He", "", "l", ""]
|
357
|
+
stream << ["lo ", "World"]
|
358
|
+
stream << []
|
359
|
+
end
|
360
|
+
assert_equal "\nHe\n\nl\n\nlo \nWorld\n\n", io.string, io.string.inspect
|
361
|
+
end
|
362
|
+
end
|
110
363
|
end
|
111
364
|
|
112
|
-
describe '
|
365
|
+
describe '#record_writer' do
|
366
|
+
describe "#write" do
|
367
|
+
it 'one block' do
|
368
|
+
io = StringIO.new
|
369
|
+
IOStreams::Stream.new(io).record_writer do |stream|
|
370
|
+
stream << {first_name: "Jack", last_name: "Johnson"}
|
371
|
+
end
|
372
|
+
assert_equal "first_name,last_name\nJack,Johnson\n", io.string, io.string.inspect
|
373
|
+
end
|
374
|
+
|
375
|
+
it 'multiple blocks' do
|
376
|
+
io = StringIO.new
|
377
|
+
IOStreams::Stream.new(io).record_writer do |stream|
|
378
|
+
stream << {first_name: "Jack", last_name: "Johnson"}
|
379
|
+
stream << {first_name: "Able", last_name: "Smith"}
|
380
|
+
end
|
381
|
+
assert_equal "first_name,last_name\nJack,Johnson\nAble,Smith\n", io.string, io.string.inspect
|
382
|
+
end
|
383
|
+
|
384
|
+
it 'empty hashes' do
|
385
|
+
io = StringIO.new
|
386
|
+
IOStreams::Stream.new(io).record_writer do |stream|
|
387
|
+
stream << {first_name: "Jack", last_name: "Johnson"}
|
388
|
+
stream << {} << {first_name: "Able", last_name: "Smith"}
|
389
|
+
stream << {}
|
390
|
+
end
|
391
|
+
assert_equal "first_name,last_name\nJack,Johnson\n\n{:first_name=>\"Able\", :last_name=>\"Smith\"}\n\n", io.string, io.string.inspect
|
392
|
+
end
|
393
|
+
|
394
|
+
it 'nil values' do
|
395
|
+
skip "TODO"
|
396
|
+
io = StringIO.new
|
397
|
+
IOStreams::Stream.new(io).record_writer do |stream|
|
398
|
+
stream << {first_name: "Jack", last_name: "Johnson"}
|
399
|
+
stream << {} << {first_name: "Able", last_name: "Smith"}
|
400
|
+
stream << {first_name: "Able", last_name: nil}
|
401
|
+
stream << {}
|
402
|
+
end
|
403
|
+
assert_equal "first_name,last_name\nJack,Johnson\n\n{:first_name=>\"Able\", :last_name=>\"Smith\"}\n\n", io.string, io.string.inspect
|
404
|
+
end
|
405
|
+
|
406
|
+
end
|
113
407
|
end
|
114
408
|
|
115
409
|
end
|
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.0.0.
|
4
|
+
version: 1.0.0.beta2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Reid Morrison
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-10-
|
11
|
+
date: 2019-10-24 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|