iostreams 0.18.0 → 0.19.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: e9c9876f8e66281fabd3a26188f1e51d23a12620de461147c9125286d58950cb
4
- data.tar.gz: 81f7b6b198f4dfa37b5349de57df74d2004e7984fd176172ac61cce2c7dac9d8
3
+ metadata.gz: 161967be051ed1b82c87f30afd32b760a2e6627cf8e090e978578c7c35aab278
4
+ data.tar.gz: abb7aab7f5aca8cc0d043ce3820603c86b3227c9db41d07420f3117010bf68af
5
5
  SHA512:
6
- metadata.gz: 91c03cfc1c218d235b2bda1ab304ebc78a8d02dbb9902372a2b5c6b5dd0afb53fcee99ed335cc9361ff2e9db9213cced9fad901a9ecf48bd56f53043792e6096
7
- data.tar.gz: 9773ffd737484920543182b8b6a32c4704cc42e83fb2be014e86447eedf184458aa6a570b129b3fa55fc44148c25862730c9af937578469467e4f54f8628ef60
6
+ metadata.gz: 2172e682359bfe1240669fee5da16f807b5a04b940529c5a7046166d5343e7c80bd20c52f05243b59c72f7cfe57ac288603ac6475e75f413f8f99031f3e1cbd0
7
+ data.tar.gz: 76923520dfda93c00b4209424496c5324ae7975b59f0f8653578652b540c07a9dd6866890b72c147ab5c968e48879210e3aaa85ab8c8ffbe4b4921524352eb32
@@ -12,6 +12,9 @@ module IOStreams
12
12
  class TypeMismatch < Error;
13
13
  end
14
14
 
15
+ class CommunicationsFailure < Error;
16
+ end
17
+
15
18
  # When the specified delimiter is not found in the supplied stream / file
16
19
  class DelimiterNotFound < Error;
17
20
  end
@@ -0,0 +1,71 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ module IOStreams
4
+ module HTTP
5
+ # Read a file using an http get.
6
+ #
7
+ # For example:
8
+ # IOStreams.reader('https://www5.fdic.gov/idasp/Offices2.zip') {|file| puts file.read}
9
+ #
10
+ # Direct example without unzipping the above file:
11
+ # IOStreams::HTTP::Reader.new('https://www5.fdic.gov/idasp/Offices2.zip') {|file| puts file.read}
12
+ #
13
+ # Parameters:
14
+ # uri: [String|URI]
15
+ # URI of the file to download.
16
+ # Example:
17
+ # https://www5.fdic.gov/idasp/Offices2.zip
18
+ #
19
+ # :username
20
+ # When supplied, basic authentication is used with the username and password.
21
+ # Default: nil
22
+ #
23
+ # :password
24
+ # Password to use use with basic authentication when the username is supplied.
25
+ #
26
+ # Notes:
27
+ # * Since Net::HTTP download only supports a push stream, the data is streamed into a tempfile first.
28
+ class Reader
29
+ def self.open(uri, username: nil, password: nil, **args, &block)
30
+ raise(ArgumentError, 'file_name must be a URI string') unless uri.is_a?(String) || uri.is_a?(URI)
31
+ handle_redirects(uri, username: username, password: password, **args, &block)
32
+ end
33
+
34
+ def self.handle_redirects(uri, username: nil, password: nil, http_redirect_count: 10, **args, &block)
35
+ uri = URI.parse(uri) unless uri.is_a?(URI)
36
+ result = nil
37
+ raise(IOStreams::Errors::CommunicationsFailure, "Too many redirects") if http_redirect_count < 1
38
+
39
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
40
+ request = Net::HTTP::Get.new(uri)
41
+ request.basic_auth(username, password) if username
42
+
43
+ http.request(request) do |response|
44
+ if response.is_a?(Net::HTTPNotFound)
45
+ raise(IOStreams::Errors::CommunicationsFailure, "Invalid URL: #{uri}")
46
+ end
47
+ if response.is_a?(Net::HTTPUnauthorized)
48
+ raise(IOStreams::Errors::CommunicationsFailure, "Authorization Required: Invalid :username or :password.")
49
+ end
50
+ if response.is_a?(Net::HTTPRedirection)
51
+ new_uri = response['location']
52
+ return handle_redirects(new_uri, username: username, password: password, http_redirect_count: http_redirect_count - 1, **args, &block)
53
+ end
54
+
55
+ raise(IOStreams::Errors::CommunicationsFailure, "Invalid response code: #{response.code}") unless response.is_a?(Net::HTTPSuccess)
56
+
57
+ # Since Net::HTTP download only supports a push stream, write it to a tempfile first.
58
+ IOStreams::Path.temp_file_name('iostreams_http') do |file_name|
59
+ IOStreams::File::Writer.open(file_name) do |io|
60
+ response.read_body { |chunk| io.write(chunk) }
61
+ end
62
+ # Return a read stream
63
+ result = IOStreams::File::Reader.open(file_name, &block)
64
+ end
65
+ end
66
+ end
67
+ result
68
+ end
69
+ end
70
+ end
71
+ end
@@ -585,8 +585,8 @@ module IOStreams
585
585
  # sftp://hostname/path/file_name
586
586
  # s3://bucket/key
587
587
  register_scheme(nil, IOStreams::File::Reader, IOStreams::File::Writer)
588
- # register_scheme(:http, IOStreams::HTTP::Reader, IOStreams::HTTP::Writer)
589
- # register_scheme(:https, IOStreams::HTTPS::Reader, IOStreams::HTTPS::Writer)
588
+ register_scheme(:http, IOStreams::HTTP::Reader, nil)
589
+ register_scheme(:https, IOStreams::HTTP::Reader, nil)
590
590
  # register_scheme(:sftp, IOStreams::SFTP::Reader, IOStreams::SFTP::Writer)
591
591
  register_scheme(:s3, IOStreams::S3::Reader, IOStreams::S3::Writer)
592
592
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+ require 'fileutils'
3
+
4
+ module IOStreams
5
+ #
6
+ # NOTE: This is a proof of concept class and will change significantly.
7
+ # I.e. Dont use it yet.
8
+ #
9
+ class Path
10
+ attr_reader :root, :relative
11
+
12
+ # Return named root path
13
+ def self.[](root)
14
+ @roots[root.to_sym] || raise(ArgumentError, "Unknown root: #{root.inspect}")
15
+ end
16
+
17
+ # Add a named root path
18
+ def self.add_root(root, path)
19
+ @roots[root.to_sym] = path.dup.freeze
20
+ end
21
+
22
+ def self.roots
23
+ @roots.dup
24
+ end
25
+
26
+ # Yields the path to a temporary file_name.
27
+ #
28
+ # File is deleted upon completion if present.
29
+ def self.temp_file_name(basename, extension = '')
30
+ result = nil
31
+ ::Dir::Tmpname.create([basename, extension]) do |tmpname|
32
+ begin
33
+ result = yield(tmpname)
34
+ ensure
35
+ ::File.unlink(tmpname) if ::File.exist?(tmpname)
36
+ end
37
+ end
38
+ result
39
+ end
40
+
41
+ def initialize(*elements, root: :default)
42
+ @root = root.to_sym
43
+ root_path = self.class[@root]
44
+ if elements.empty?
45
+ @relative = ''
46
+ @path = root_path
47
+ else
48
+ @relative = ::File.join(*elements).freeze
49
+ if @relative.start_with?(root_path)
50
+ @path = @relative
51
+ @relative = @path[root_path.size + 1..-1].freeze
52
+ else
53
+ @path = ::File.join(root_path, @relative).freeze
54
+ end
55
+ end
56
+ end
57
+
58
+ def to_s
59
+ @path
60
+ end
61
+
62
+ # Creates the entire path excluding the file_name.
63
+ def mkpath
64
+ path = ::File.dirname(@path)
65
+ FileUtils.mkdir_p(path) unless ::File.exist?(path)
66
+ self
67
+ end
68
+
69
+ def exist?
70
+ ::File.exist?(@path)
71
+ end
72
+
73
+ # Delete the file.
74
+ #
75
+ # Note: Only the file is removed, not any of the parent paths.
76
+ def delete
77
+ ::File.unlink(@path)
78
+ self
79
+ end
80
+
81
+ private
82
+
83
+ @roots = {}
84
+ end
85
+ end
@@ -13,15 +13,13 @@ module IOStreams
13
13
 
14
14
  begin
15
15
  # Since S3 download only supports a push stream, write it to a tempfile first.
16
- temp_file = Tempfile.new('rocket_job')
17
- temp_file.binmode
16
+ IOStreams::Path.temp_file_name('iostreams_s3') do |file_name|
17
+ args[:response_target] = file_name
18
+ object.get(args)
18
19
 
19
- args[:response_target] = temp_file.to_path
20
- object.get(args)
21
-
22
- block.call(temp_file)
23
- ensure
24
- temp_file.delete if temp_file
20
+ # Return a read stream
21
+ IOStreams::File::Reader.open(file_name, &block)
22
+ end
25
23
  end
26
24
  end
27
25
  end
@@ -1,3 +1,3 @@
1
1
  module IOStreams
2
- VERSION = '0.18.0'
2
+ VERSION = '0.19.0'
3
3
  end
@@ -4,24 +4,24 @@ module IOStreams
4
4
  module Xlsx
5
5
  class Reader
6
6
  # Convert a xlsx, or xlsm file or stream into CSV format.
7
- def self.open(file_name_or_io, _ = nil)
8
- if file_name_or_io.is_a?(String)
9
- file_name = file_name_or_io
10
- else
11
- temp_file = Tempfile.new('iostreams_xlsx')
12
- temp_file.binmode
13
- IOStreams.copy(file_name_or_io, temp_file)
14
- file_name = temp_file.to_path
7
+ def self.open(file_name_or_io, _ = nil, &block)
8
+ return extract_csv(file_name_or_io, &block) if file_name_or_io.is_a?(String)
9
+
10
+ # Creek gem can only work against a file, not a stream, so create temp file.
11
+ IOStreams::Path.temp_file_name('iostreams_xlsx') do |temp_file_name|
12
+ IOStreams.copy(file_name_or_io, temp_file_name, target_options: {streams: []})
13
+ extract_csv(temp_file_name, &block)
15
14
  end
15
+ end
16
16
 
17
- csv_temp_file = Tempfile.new('iostreams_csv')
18
- csv_temp_file.binmode
19
- new(file_name).each { |lines| csv_temp_file << lines.to_csv }
20
- csv_temp_file.rewind
21
- yield csv_temp_file
22
- ensure
23
- temp_file.delete if temp_file
24
- csv_temp_file.delete if csv_temp_file
17
+ # Convert the spreadsheet to csv in a tempfile
18
+ def self.extract_csv(file_name, &block)
19
+ IOStreams::Path.temp_file_name('iostreams_csv') do |temp_file_name|
20
+ IOStreams::File::Writer.open(temp_file_name) do |io|
21
+ new(file_name).each { |lines| io << lines.to_csv }
22
+ end
23
+ IOStreams::File::Reader.open(temp_file_name, &block)
24
+ end
25
25
  end
26
26
 
27
27
  def initialize(file_name)
@@ -12,7 +12,7 @@ module IOStreams
12
12
  # puts data
13
13
  # end
14
14
  # end
15
- def self.open(file_name_or_io, buffer_size: 65536, &block)
15
+ def self.open(file_name_or_io, _ = nil, &block)
16
16
  if !defined?(JRuby) && !defined?(::Zip)
17
17
  # MRI needs Ruby Zip, since it only has native support for GZip
18
18
  begin
@@ -25,21 +25,10 @@ module IOStreams
25
25
  # File name supplied
26
26
  return read_file(file_name_or_io, &block) unless IOStreams.reader_stream?(file_name_or_io)
27
27
 
28
- # Stream supplied
29
- begin
30
- # Since ZIP cannot be streamed, download un-zipped data to a local file before streaming
31
- temp_file = Tempfile.new('rocket_job')
32
- temp_file.binmode
33
- file_name = temp_file.to_path
34
-
35
- # Stream zip stream into temp file
36
- ::File.open(file_name, 'wb') do |file|
37
- IOStreams.copy(file_name_or_io, file, buffer_size: buffer_size)
38
- end
39
-
40
- read_file(file_name, &block)
41
- ensure
42
- temp_file.delete if temp_file
28
+ # ZIP can only work against a file, not a stream, so create temp file.
29
+ IOStreams::Path.temp_file_name('iostreams_zip') do |temp_file_name|
30
+ IOStreams.copy(file_name_or_io, temp_file_name, target_options: {streams: []})
31
+ read_file(temp_file_name, &block)
43
32
  end
44
33
  end
45
34
 
@@ -38,17 +38,10 @@ module IOStreams
38
38
  # File name supplied
39
39
  return write_file(file_name_or_io, zip_file_name, &block) unless IOStreams.writer_stream?(file_name_or_io)
40
40
 
41
- # Stream supplied
42
- begin
43
- # Since ZIP cannot be streamed, download to a local file before streaming
44
- temp_file = Tempfile.new('rocket_job')
45
- temp_file.binmode
46
- write_file(temp_file.to_path, zip_file_name, &block)
47
-
48
- # Stream temp file into output stream
49
- IOStreams.copy(temp_file, file_name_or_io, buffer_size: buffer_size)
50
- ensure
51
- temp_file.delete if temp_file
41
+ # ZIP can only work against a file, not a stream, so create temp file.
42
+ IOStreams::Path.temp_file_name('iostreams_zip') do |temp_file_name|
43
+ write_file(temp_file_name, zip_file_name, &block)
44
+ IOStreams.copy(temp_file_name, file_name_or_io, source_options: {streams: []})
52
45
  end
53
46
  end
54
47
 
@@ -15,6 +15,10 @@ module IOStreams
15
15
  autoload :Reader, 'io_streams/gzip/reader'
16
16
  autoload :Writer, 'io_streams/gzip/writer'
17
17
  end
18
+ module HTTP
19
+ autoload :Reader, 'io_streams/http/reader'
20
+ end
21
+ autoload :Path, 'io_streams/path'
18
22
  autoload :Pgp, 'io_streams/pgp'
19
23
  autoload :S3, 'io_streams/s3'
20
24
  module SFTP
@@ -0,0 +1,38 @@
1
+ require_relative 'test_helper'
2
+
3
+ class HTTPReaderTest < Minitest::Test
4
+ describe IOStreams::HTTP::Reader do
5
+ let :uri do
6
+ "http://example.com/index.html?count=10"
7
+ end
8
+
9
+ let :ssl_uri do
10
+ "https://example.com/index.html?count=10"
11
+ end
12
+
13
+ describe '.open' do
14
+ it 'reads http' do
15
+ result = IOStreams::HTTP::Reader.open(uri) do |io|
16
+ io.read
17
+ end
18
+ assert_includes result, "<html>"
19
+ end
20
+
21
+ it 'reads https' do
22
+ result = IOStreams::HTTP::Reader.open(ssl_uri) do |io|
23
+ io.read
24
+ end
25
+ assert_includes result, "<html>"
26
+ end
27
+
28
+ it 'does not support streams' do
29
+ assert_raises ArgumentError do
30
+ io = StringIO.new
31
+ IOStreams::HTTP::Reader.open(io) do |http_io|
32
+ http_io.read
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,74 @@
1
+ require_relative 'test_helper'
2
+
3
+ module IOStreams
4
+ class PathTest < Minitest::Test
5
+ describe IOStreams::Path do
6
+ describe '.root' do
7
+ it 'return default path' do
8
+ path = ::File.expand_path(::File.join(__dir__, '../tmp/default'))
9
+ assert_equal path, IOStreams::Path[:default]
10
+ end
11
+
12
+ it 'return downloads path' do
13
+ path = ::File.expand_path(::File.join(__dir__, '../tmp/downloads'))
14
+ assert_equal path, IOStreams::Path[:downloads]
15
+ end
16
+ end
17
+
18
+ describe '.to_s' do
19
+ it 'returns path' do
20
+ assert_equal IOStreams::Path[:default], IOStreams::Path.new.to_s
21
+ end
22
+
23
+ it 'adds path to root' do
24
+ assert_equal ::File.join(IOStreams::Path[:default], 'test'), IOStreams::Path.new('test').to_s
25
+ end
26
+
27
+ it 'adds paths to root' do
28
+ assert_equal ::File.join(IOStreams::Path[:default], 'test', 'second', 'third'), IOStreams::Path.new('test', 'second', 'third').to_s
29
+ end
30
+
31
+ it 'returns path and filename' do
32
+ path = ::File.join(IOStreams::Path[:default], 'file.xls')
33
+ assert_equal path, IOStreams::Path.new('file.xls').to_s
34
+ end
35
+
36
+ it 'adds path to root and filename' do
37
+ path = ::File.join(IOStreams::Path[:default], 'test', 'file.xls')
38
+ assert_equal path, IOStreams::Path.new('test', 'file.xls').to_s
39
+ end
40
+
41
+ it 'adds paths to root' do
42
+ path = ::File.join(IOStreams::Path[:default], 'test', 'second', 'third', 'file.xls')
43
+ assert_equal path, IOStreams::Path.new('test', 'second', 'third', 'file.xls').to_s
44
+ end
45
+
46
+ it 'return path as sent in when full path' do
47
+ path = ::File.join(IOStreams::Path[:default], 'file.xls')
48
+ assert_equal path, IOStreams::Path.new(path).to_s
49
+ end
50
+ end
51
+
52
+ describe '.mkpath' do
53
+ it 'makes root' do
54
+ path = IOStreams::Path.new('test.xls')
55
+ assert_equal path, path.mkpath
56
+ assert ::File.exist?(IOStreams::Path.new.to_s)
57
+ end
58
+
59
+ it 'makes root with path' do
60
+ path = IOStreams::Path.new('test', 'test.xls')
61
+ assert_equal path, path.mkpath
62
+ assert ::File.exist?(IOStreams::Path.new('test').to_s)
63
+ end
64
+
65
+ it 'makes root with paths' do
66
+ path = IOStreams::Path.new('test', 'second', 'third', 'test.xls')
67
+ assert_equal path, path.mkpath
68
+ assert ::File.exist?(IOStreams::Path.new('test', 'second', 'third').to_s)
69
+ end
70
+ end
71
+
72
+ end
73
+ end
74
+ end
@@ -32,3 +32,8 @@ unless IOStreams::Pgp.has_key?(email: 'receiver@example.org')
32
32
  puts 'Generating test PGP key: receiver@example.org'
33
33
  IOStreams::Pgp.generate_key(name: 'Receiver', email: 'receiver@example.org', passphrase: 'receiver_passphrase', key_length: 2048)
34
34
  end
35
+
36
+ # Test paths
37
+ root = File.expand_path(File.join(__dir__, '../tmp'))
38
+ IOStreams::Path.add_root(:default, File.join(root, 'default'))
39
+ IOStreams::Path.add_root(:downloads, File.join(root, 'downloads'))
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: 0.18.0
4
+ version: 0.19.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: 2019-08-15 00:00:00.000000000 Z
11
+ date: 2019-08-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -43,9 +43,11 @@ files:
43
43
  - lib/io_streams/file/writer.rb
44
44
  - lib/io_streams/gzip/reader.rb
45
45
  - lib/io_streams/gzip/writer.rb
46
+ - lib/io_streams/http/reader.rb
46
47
  - lib/io_streams/io_streams.rb
47
48
  - lib/io_streams/line/reader.rb
48
49
  - lib/io_streams/line/writer.rb
50
+ - lib/io_streams/path.rb
49
51
  - lib/io_streams/pgp.rb
50
52
  - lib/io_streams/pgp/reader.rb
51
53
  - lib/io_streams/pgp/writer.rb
@@ -93,9 +95,11 @@ files:
93
95
  - test/files/unclosed_quote_test.csv
94
96
  - test/gzip_reader_test.rb
95
97
  - test/gzip_writer_test.rb
98
+ - test/http_reader_test.rb
96
99
  - test/io_streams_test.rb
97
100
  - test/line_reader_test.rb
98
101
  - test/line_writer_test.rb
102
+ - test/path_test.rb
99
103
  - test/pgp_reader_test.rb
100
104
  - test/pgp_test.rb
101
105
  - test/pgp_writer_test.rb
@@ -145,6 +149,7 @@ test_files:
145
149
  - test/file_reader_test.rb
146
150
  - test/record_reader_test.rb
147
151
  - test/s3_writer_test.rb
152
+ - test/http_reader_test.rb
148
153
  - test/pgp_writer_test.rb
149
154
  - test/line_writer_test.rb
150
155
  - test/row_reader_test.rb
@@ -165,6 +170,7 @@ test_files:
165
170
  - test/test_helper.rb
166
171
  - test/file_writer_test.rb
167
172
  - test/tabular_test.rb
173
+ - test/path_test.rb
168
174
  - test/pgp_test.rb
169
175
  - test/io_streams_test.rb
170
176
  - test/record_writer_test.rb