iostreams 0.18.0 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
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