iostreams 1.8.0 → 1.10.2
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/builder.rb +20 -7
- data/lib/io_streams/gzip/reader.rb +2 -2
- data/lib/io_streams/line/reader.rb +4 -3
- data/lib/io_streams/paths/file.rb +5 -0
- data/lib/io_streams/paths/s3.rb +5 -0
- data/lib/io_streams/paths/sftp.rb +5 -0
- data/lib/io_streams/pgp/writer.rb +3 -1
- data/lib/io_streams/stream.rb +8 -0
- data/lib/io_streams/tabular/parser/fixed.rb +2 -2
- data/lib/io_streams/version.rb +1 -1
- data/lib/io_streams/writer.rb +2 -1
- data/test/builder_test.rb +15 -0
- data/test/files/utf16_test.csv +0 -0
- data/test/paths/file_test.rb +16 -1
- data/test/row_reader_test.rb +1 -1
- data/test/tabular_test.rb +18 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 26d07bc98819ce065e8c698f00b75a6c2e9fc0a9ea6bf13c1afa8bc8c1e71e66
|
4
|
+
data.tar.gz: 2f7898b4197d3f94cbfcc30ba6c8d23e72121877833e57ec9a989e465d019ca2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2887fb5b2935d28bcf2426fd7139a6b7a27c87c1da8d7289cc69f0a644c2d98e4667f6e6da1a57133bcf8bdd93e948d7819d24ce09cb0d65f6c35d59b0b604c7
|
7
|
+
data.tar.gz: 034aa07ff80107a139dd86ffd492361887de4f4e8190385b48d12b1bedeb0a3af8c1a25720d41f731330cb46d707736972c707c3ec5576551425d29d3843da68
|
data/lib/io_streams/builder.rb
CHANGED
@@ -79,15 +79,16 @@ module IOStreams
|
|
79
79
|
# with their options that will be applied when the reader or writer is invoked.
|
80
80
|
def pipeline
|
81
81
|
return streams.dup.freeze if streams
|
82
|
-
return {}.freeze unless file_name
|
83
82
|
|
84
|
-
|
85
|
-
|
86
|
-
built_streams[:encode] = options[:encode] if options&.key?(:encode)
|
83
|
+
build_pipeline.freeze
|
84
|
+
end
|
87
85
|
|
88
|
-
|
89
|
-
|
90
|
-
|
86
|
+
# Removes the named stream from the current pipeline.
|
87
|
+
# If the stream pipeline has not yet been built it will be built from the file_name if present.
|
88
|
+
# Note: Any options must be set _before_ calling this method.
|
89
|
+
def remove_from_pipeline(stream_name)
|
90
|
+
@streams ||= build_pipeline
|
91
|
+
@streams.delete(stream_name.to_sym)
|
91
92
|
end
|
92
93
|
|
93
94
|
# Returns the tabular format if set, otherwise tries to autodetect the format if the file_name has been set
|
@@ -106,6 +107,18 @@ module IOStreams
|
|
106
107
|
|
107
108
|
private
|
108
109
|
|
110
|
+
def build_pipeline
|
111
|
+
return {} unless file_name
|
112
|
+
|
113
|
+
built_streams = {}
|
114
|
+
# Encode stream is always first
|
115
|
+
built_streams[:encode] = options[:encode] if options&.key?(:encode)
|
116
|
+
|
117
|
+
opts = options || {}
|
118
|
+
parse_extensions.each { |stream| built_streams[stream] = opts[stream] || {} }
|
119
|
+
built_streams
|
120
|
+
end
|
121
|
+
|
109
122
|
def class_for_stream(type, stream)
|
110
123
|
ext = IOStreams.extensions[stream.nil? ? nil : stream.to_sym] ||
|
111
124
|
raise(ArgumentError, "Unknown Stream type: #{stream.inspect}")
|
@@ -2,9 +2,9 @@ module IOStreams
|
|
2
2
|
module Gzip
|
3
3
|
class Reader < IOStreams::Reader
|
4
4
|
# Read from a gzip stream, decompressing the contents as it is read
|
5
|
-
def self.stream(input_stream, original_file_name: nil
|
5
|
+
def self.stream(input_stream, original_file_name: nil)
|
6
6
|
io = ::Zlib::GzipReader.new(input_stream)
|
7
|
-
|
7
|
+
yield io
|
8
8
|
ensure
|
9
9
|
io&.close
|
10
10
|
end
|
@@ -146,8 +146,8 @@ module IOStreams
|
|
146
146
|
data
|
147
147
|
end
|
148
148
|
|
149
|
-
# Returns
|
150
|
-
# Returns
|
149
|
+
# Returns whether more data is available to read
|
150
|
+
# Returns false on EOF
|
151
151
|
def read_block
|
152
152
|
return false if @eof
|
153
153
|
|
@@ -157,7 +157,8 @@ module IOStreams
|
|
157
157
|
@input_stream.read(@buffer_size, @read_cache_buffer)
|
158
158
|
rescue ArgumentError
|
159
159
|
# Handle arity of -1 when just 0..1
|
160
|
-
@read_cache_buffer
|
160
|
+
@read_cache_buffer = nil
|
161
|
+
@use_read_cache_buffer = false
|
161
162
|
@input_stream.read(@buffer_size)
|
162
163
|
end
|
163
164
|
else
|
@@ -89,6 +89,11 @@ module IOStreams
|
|
89
89
|
# "**.rb" "lib/song.rb" true
|
90
90
|
# "*" "dave/.profile" true
|
91
91
|
def each_child(pattern = "*", case_sensitive: false, directories: false, hidden: false)
|
92
|
+
unless block_given?
|
93
|
+
return to_enum(__method__, pattern,
|
94
|
+
case_sensitive: case_sensitive, directories: directories, hidden: hidden)
|
95
|
+
end
|
96
|
+
|
92
97
|
flags = 0
|
93
98
|
flags |= ::File::FNM_CASEFOLD unless case_sensitive
|
94
99
|
flags |= ::File::FNM_DOTMATCH if hidden
|
data/lib/io_streams/paths/s3.rb
CHANGED
@@ -284,6 +284,11 @@ module IOStreams
|
|
284
284
|
# Notes:
|
285
285
|
# - Currently all S3 lookups are recursive as of the pattern regardless of whether the pattern includes `**`.
|
286
286
|
def each_child(pattern = "*", case_sensitive: false, directories: false, hidden: false)
|
287
|
+
unless block_given?
|
288
|
+
return to_enum(__method__, pattern,
|
289
|
+
case_sensitive: case_sensitive, directories: directories, hidden: hidden)
|
290
|
+
end
|
291
|
+
|
287
292
|
matcher = Matcher.new(self, pattern, case_sensitive: case_sensitive, hidden: hidden)
|
288
293
|
|
289
294
|
# When the pattern includes an exact file name without any pattern characters
|
@@ -142,6 +142,11 @@ module IOStreams
|
|
142
142
|
# sftp://sftp.example.org/a/b/c/test.txt {:type=>1, :size=>37, :owner=>"test_owner", :group=>"test_group",
|
143
143
|
# :permissions=>420, :atime=>1572378136, :mtime=>1572378136, :link_count=>1, :extended=>{}}
|
144
144
|
def each_child(pattern = "*", case_sensitive: true, directories: false, hidden: false)
|
145
|
+
unless block_given?
|
146
|
+
return to_enum(__method__, pattern,
|
147
|
+
case_sensitive: case_sensitive, directories: directories, hidden: hidden)
|
148
|
+
end
|
149
|
+
|
145
150
|
Utils.load_soft_dependency("net-sftp", "SFTP glob capability", "net/sftp") unless defined?(Net::SFTP)
|
146
151
|
|
147
152
|
flags = ::File::FNM_EXTGLOB
|
@@ -89,10 +89,11 @@ module IOStreams
|
|
89
89
|
|
90
90
|
IOStreams::Pgp.logger&.debug { "IOStreams::Pgp::Writer.open: #{command}" }
|
91
91
|
|
92
|
+
result = nil
|
92
93
|
Open3.popen2e(command) do |stdin, out, waith_thr|
|
93
94
|
begin
|
94
95
|
stdin.binmode
|
95
|
-
yield(stdin)
|
96
|
+
result = yield(stdin)
|
96
97
|
stdin.close
|
97
98
|
rescue Errno::EPIPE
|
98
99
|
# Ignore broken pipe because gpg terminates early due to an error
|
@@ -104,6 +105,7 @@ module IOStreams
|
|
104
105
|
raise(Pgp::Failure, "GPG Failed to create encrypted file: #{file_name}: #{out.read.chomp}")
|
105
106
|
end
|
106
107
|
end
|
108
|
+
result
|
107
109
|
end
|
108
110
|
end
|
109
111
|
end
|
data/lib/io_streams/stream.rb
CHANGED
@@ -56,6 +56,14 @@ module IOStreams
|
|
56
56
|
builder.pipeline
|
57
57
|
end
|
58
58
|
|
59
|
+
# Removes the named stream from the current pipeline.
|
60
|
+
# If the stream pipeline has not yet been built it will be built from the file_name if present.
|
61
|
+
# Note: Any options must be set _before_ calling this method.
|
62
|
+
def remove_from_pipeline(stream_name)
|
63
|
+
builder.remove_from_pipeline(stream_name)
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
59
67
|
# Iterate over a file / stream returning one line at a time.
|
60
68
|
#
|
61
69
|
# Example: Read a line at a time
|
@@ -148,14 +148,14 @@ module IOStreams
|
|
148
148
|
|
149
149
|
def initialize(size:, key: nil, type: :string, decimals: 2)
|
150
150
|
@key = key
|
151
|
-
@size = size == :remainder ? -1 : size.to_i
|
151
|
+
@size = (size == :remainder || size == "remainder") ? -1 : size.to_i
|
152
152
|
@type = type.to_sym
|
153
153
|
@decimals = decimals
|
154
154
|
|
155
155
|
unless @size.positive? || (@size == -1)
|
156
156
|
raise(Errors::InvalidLayout, "Size #{size.inspect} must be positive or :remainder")
|
157
157
|
end
|
158
|
-
raise(Errors::InvalidLayout, "Unknown type: #{type.inspect}") unless TYPES.include?(type)
|
158
|
+
raise(Errors::InvalidLayout, "Unknown type: #{type.inspect}") unless TYPES.include?(@type)
|
159
159
|
end
|
160
160
|
|
161
161
|
def parse(value)
|
data/lib/io_streams/version.rb
CHANGED
data/lib/io_streams/writer.rb
CHANGED
@@ -4,8 +4,9 @@ module IOStreams
|
|
4
4
|
# and then pass that filename in for this reader.
|
5
5
|
def self.stream(output_stream, original_file_name: nil, **args, &block)
|
6
6
|
Utils.temp_file_name("iostreams_writer") do |file_name|
|
7
|
-
file(file_name, original_file_name: original_file_name, **args, &block)
|
7
|
+
count = file(file_name, original_file_name: original_file_name, **args, &block)
|
8
8
|
::File.open(file_name, "rb") { |source| ::IO.copy_stream(source, output_stream) }
|
9
|
+
count
|
9
10
|
end
|
10
11
|
end
|
11
12
|
|
data/test/builder_test.rb
CHANGED
@@ -237,6 +237,21 @@ class BuilderTest < Minitest::Test
|
|
237
237
|
end
|
238
238
|
end
|
239
239
|
|
240
|
+
describe "#remove_from_pipeline" do
|
241
|
+
let(:file_name) { "my/path/abc.bz2.pgp" }
|
242
|
+
it "removes a named stream from the pipeline" do
|
243
|
+
assert_equal({bz2: {}, pgp: {}}, streams.pipeline)
|
244
|
+
streams.remove_from_pipeline(:bz2)
|
245
|
+
assert_equal({pgp: {}}, streams.pipeline)
|
246
|
+
end
|
247
|
+
it "removes a named stream from the pipeline with options" do
|
248
|
+
streams.option(:pgp, passphrase: "unlock-me")
|
249
|
+
assert_equal({bz2: {}, pgp: {passphrase: "unlock-me"}}, streams.pipeline)
|
250
|
+
streams.remove_from_pipeline(:bz2)
|
251
|
+
assert_equal({pgp: {passphrase: "unlock-me"}}, streams.pipeline)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
240
255
|
describe "#execute" do
|
241
256
|
it "directly calls block for an empty stream" do
|
242
257
|
string_io = StringIO.new
|
Binary file
|
data/test/paths/file_test.rb
CHANGED
@@ -5,7 +5,7 @@ module Paths
|
|
5
5
|
describe IOStreams::Paths::File do
|
6
6
|
let(:root) { IOStreams::Paths::File.new("/tmp/iostreams").delete_all }
|
7
7
|
let(:directory) { root.join("/some_test_dir") }
|
8
|
-
let(:data) { "Hello World" }
|
8
|
+
let(:data) { "Hello World\nHow are you doing?\nOn this fine day" }
|
9
9
|
let(:file_path) do
|
10
10
|
path = root.join("some_test_dir/test_file.txt")
|
11
11
|
path.writer { |io| io << data }
|
@@ -17,6 +17,15 @@ module Paths
|
|
17
17
|
path
|
18
18
|
end
|
19
19
|
|
20
|
+
describe "#each" do
|
21
|
+
it "reads lines" do
|
22
|
+
records = []
|
23
|
+
count = file_path.each { |line| records << line }
|
24
|
+
assert_equal count, data.lines.size
|
25
|
+
assert_equal data.lines.collect(&:strip), records
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
20
29
|
describe "#each_child" do
|
21
30
|
it "iterates an empty path" do
|
22
31
|
none = nil
|
@@ -48,6 +57,12 @@ module Paths
|
|
48
57
|
actual = root.children("**/Test*.TXT", case_sensitive: true).collect(&:to_s)
|
49
58
|
refute_equal expected, actual.sort
|
50
59
|
end
|
60
|
+
|
61
|
+
it "with no block returns enumerator" do
|
62
|
+
expected = [file_path.to_s, file_path2.to_s]
|
63
|
+
actual = root.each_child("**/*").first(100).collect(&:to_s)
|
64
|
+
assert_equal expected.sort, actual.sort
|
65
|
+
end
|
51
66
|
end
|
52
67
|
|
53
68
|
describe "#mkpath" do
|
data/test/row_reader_test.rb
CHANGED
data/test/tabular_test.rb
CHANGED
@@ -40,6 +40,19 @@ class TabularTest < Minitest::Test
|
|
40
40
|
IOStreams::Tabular.new(format: :fixed, format_options: {layout: layout})
|
41
41
|
end
|
42
42
|
|
43
|
+
let :fixed_with_strings do
|
44
|
+
layout = [
|
45
|
+
{size: "23", key: "name"},
|
46
|
+
{size: 40, key: "address"},
|
47
|
+
{size: 2},
|
48
|
+
{size: 5.0, key: "zip", type: "integer"},
|
49
|
+
{size: "8", key: "age", type: "integer"},
|
50
|
+
{size: 10, key: "weight", type: "float", decimals: 2},
|
51
|
+
{size: "remainder", key: "remainder"}
|
52
|
+
]
|
53
|
+
IOStreams::Tabular.new(format: :fixed, format_options: {layout: layout})
|
54
|
+
end
|
55
|
+
|
43
56
|
describe "#parse_header" do
|
44
57
|
it "parses and sets the csv header" do
|
45
58
|
tabular = IOStreams::Tabular.new(format: :csv)
|
@@ -269,6 +282,11 @@ class TabularTest < Minitest::Test
|
|
269
282
|
assert_equal "Jack over there 34618000000210123456.79", string
|
270
283
|
end
|
271
284
|
|
285
|
+
it "renders fixed data with string keys" do
|
286
|
+
assert string = fixed_with_strings.render("name" => "Jack", "address" => "over there", "zip" => 34_618, "weight" => 123_456.789123, "age" => 21)
|
287
|
+
assert_equal "Jack over there 34618000000210123456.79", string
|
288
|
+
end
|
289
|
+
|
272
290
|
it "truncates long strings" do
|
273
291
|
assert string = fixed.render(name: "Jack ran up the beanstalk and when jack reached the top it was truncated", address: "over there", zip: 34_618)
|
274
292
|
assert_equal "Jack ran up the beanstaover there 34618000000000000000.00", string
|
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.
|
4
|
+
version: 1.10.2
|
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-
|
11
|
+
date: 2021-10-25 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -87,6 +87,7 @@ files:
|
|
87
87
|
- test/files/unclosed_quote_large_test.csv
|
88
88
|
- test/files/unclosed_quote_test.csv
|
89
89
|
- test/files/unclosed_quote_test2.csv
|
90
|
+
- test/files/utf16_test.csv
|
90
91
|
- test/gzip_reader_test.rb
|
91
92
|
- test/gzip_writer_test.rb
|
92
93
|
- test/io_streams_test.rb
|
@@ -159,6 +160,7 @@ test_files:
|
|
159
160
|
- test/files/unclosed_quote_large_test.csv
|
160
161
|
- test/files/unclosed_quote_test.csv
|
161
162
|
- test/files/unclosed_quote_test2.csv
|
163
|
+
- test/files/utf16_test.csv
|
162
164
|
- test/gzip_reader_test.rb
|
163
165
|
- test/gzip_writer_test.rb
|
164
166
|
- test/io_streams_test.rb
|