iostreams 1.0.0.beta → 1.0.0.beta2
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 +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:
|