exel 1.2.1 → 1.5.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.
Files changed (66) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +4 -4
  3. data/.gitignore +1 -2
  4. data/.rubocop.yml +23 -14
  5. data/.rubocop_airbnb.yml +2 -0
  6. data/.rubocop_todo.yml +1 -13
  7. data/.travis.yml +26 -0
  8. data/Gemfile +2 -2
  9. data/Gemfile.lock +118 -0
  10. data/Guardfile +1 -0
  11. data/README.md +96 -31
  12. data/Rakefile +2 -0
  13. data/exel.gemspec +7 -7
  14. data/lib/exel.rb +7 -1
  15. data/lib/exel/ast_node.rb +6 -10
  16. data/lib/exel/context.rb +4 -1
  17. data/lib/exel/deferred_context_value.rb +3 -1
  18. data/lib/exel/error/job_termination.rb +12 -0
  19. data/lib/exel/events.rb +6 -0
  20. data/lib/exel/instruction.rb +5 -2
  21. data/lib/exel/instruction_node.rb +2 -0
  22. data/lib/exel/job.rb +8 -4
  23. data/lib/exel/listen_instruction.rb +2 -0
  24. data/lib/exel/logging.rb +24 -1
  25. data/lib/exel/logging/logger_wrapper.rb +31 -0
  26. data/lib/exel/logging_helper.rb +36 -0
  27. data/lib/exel/middleware/chain.rb +67 -0
  28. data/lib/exel/middleware/logging.rb +30 -0
  29. data/lib/exel/null_instruction.rb +2 -0
  30. data/lib/exel/processor_helper.rb +9 -1
  31. data/lib/exel/processors/async_processor.rb +2 -8
  32. data/lib/exel/processors/run_processor.rb +2 -6
  33. data/lib/exel/processors/split_processor.rb +15 -10
  34. data/lib/exel/providers/local_file_provider.rb +9 -6
  35. data/lib/exel/providers/threaded_async_provider.rb +2 -0
  36. data/lib/exel/remote_value.rb +11 -0
  37. data/lib/exel/sequence_node.rb +2 -0
  38. data/lib/exel/value.rb +2 -0
  39. data/lib/exel/version.rb +3 -1
  40. data/spec/exel/ast_node_spec.rb +48 -27
  41. data/spec/exel/context_spec.rb +77 -77
  42. data/spec/exel/deferred_context_value_spec.rb +42 -42
  43. data/spec/exel/events_spec.rb +68 -59
  44. data/spec/exel/instruction_node_spec.rb +17 -16
  45. data/spec/exel/instruction_spec.rb +49 -42
  46. data/spec/exel/job_spec.rb +99 -84
  47. data/spec/exel/listen_instruction_spec.rb +11 -10
  48. data/spec/exel/logging/logger_wrapper_spec.rb +93 -0
  49. data/spec/exel/logging_helper_spec.rb +24 -0
  50. data/spec/exel/logging_spec.rb +69 -24
  51. data/spec/exel/middleware/chain_spec.rb +65 -0
  52. data/spec/exel/middleware/logging_spec.rb +31 -0
  53. data/spec/exel/middleware_spec.rb +68 -0
  54. data/spec/exel/null_instruction_spec.rb +4 -4
  55. data/spec/exel/processors/async_processor_spec.rb +17 -18
  56. data/spec/exel/processors/run_processor_spec.rb +10 -11
  57. data/spec/exel/processors/split_processor_spec.rb +99 -74
  58. data/spec/exel/providers/local_file_provider_spec.rb +26 -28
  59. data/spec/exel/providers/threaded_async_provider_spec.rb +37 -38
  60. data/spec/exel/sequence_node_spec.rb +12 -11
  61. data/spec/exel/value_spec.rb +33 -33
  62. data/spec/exel_spec.rb +9 -7
  63. data/spec/integration/integration_spec.rb +3 -1
  64. data/spec/spec_helper.rb +4 -2
  65. data/spec/support/integration_test_classes.rb +4 -3
  66. metadata +37 -48
@@ -1,24 +1,18 @@
1
- require_relative '../processor_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module EXEL
4
4
  module Processors
5
5
  # Implements the +async+ instruction by using the configured async provider to run a block asynchronously.
6
6
  class AsyncProcessor
7
- include EXEL::ProcessorHelper
8
7
  attr_reader :provider
9
8
 
10
9
  def initialize(context)
11
10
  @context = context
12
11
  @provider = EXEL.async_provider.new(context)
13
-
14
- log_prefix_with '[AsyncProcessor]'
15
12
  end
16
13
 
17
14
  def process(block)
18
- log_process do
19
- @provider.do_async(block)
20
- log_info 'call to async completed'
21
- end
15
+ @provider.do_async(block)
22
16
  end
23
17
  end
24
18
  end
@@ -1,11 +1,9 @@
1
- require_relative '../processor_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module EXEL
4
4
  module Processors
5
5
  # Implements the +run+ instruction.
6
6
  class RunProcessor
7
- include EXEL::ProcessorHelper
8
-
9
7
  # Requires +context[:job]+ to contain the name of the job to be run.
10
8
  def initialize(context)
11
9
  @context = context
@@ -13,9 +11,7 @@ module EXEL
13
11
 
14
12
  # Runs the specified job with the current context
15
13
  def process(_block = nil)
16
- log_process "running job #{@context[:job]}" do
17
- EXEL::Job.run(@context[:job], @context)
18
- end
14
+ EXEL::Job.run(@context[:job], @context)
19
15
  end
20
16
  end
21
17
  end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'csv'
2
4
  require 'tempfile'
3
- require_relative '../processor_helper'
5
+ require_relative '../logging_helper'
4
6
 
5
7
  module EXEL
6
8
  module Processors
@@ -11,8 +13,10 @@ module EXEL
11
13
  # * +:delete_resource+ Defaults to true, can be set to false to preserve the original resource. Otherwise, it will
12
14
  # be deleted when splitting is complete
13
15
  # * +:chunk_size+ Set to specify the number of lines that each chunk should contain
16
+ # * +:max_chunks+ Set to specify the maximum number of chunks that should be processed. The resource will not be
17
+ # consumed beyond this limit.
14
18
  class SplitProcessor
15
- include EXEL::ProcessorHelper
19
+ include EXEL::LoggingHelper
16
20
 
17
21
  attr_accessor :file_name, :block
18
22
 
@@ -25,16 +29,16 @@ module EXEL
25
29
  @tempfile_count = 0
26
30
  @context = context
27
31
  @file = context[:resource]
32
+ @max_chunks = @context[:max_chunks] || Float::INFINITY
28
33
  @context[:delete_resource] = true if @context[:delete_resource].nil?
29
-
30
- log_prefix_with '[SplitProcessor]'
31
34
  end
32
35
 
33
36
  def process(callback)
34
- log_process do
35
- process_file(callback)
36
- finish(callback)
37
- end
37
+ process_file(callback)
38
+ finish(callback)
39
+ ensure
40
+ @file.close
41
+ File.delete(@file.path) if @context[:delete_resource]
38
42
  end
39
43
 
40
44
  def process_line(line, callback)
@@ -62,8 +66,10 @@ module EXEL
62
66
  def process_file(callback)
63
67
  csv_options = @context[:csv_options] || {col_sep: ','}
64
68
 
65
- CSV.foreach(@file.path, csv_options) do |line|
69
+ CSV.foreach(@file.path, **csv_options) do |line|
66
70
  process_line(line, callback)
71
+
72
+ break if @tempfile_count == @max_chunks
67
73
  end
68
74
  rescue CSV::MalformedCSVError => e
69
75
  log_error "CSV::MalformedCSVError => #{e.message}"
@@ -97,7 +103,6 @@ module EXEL
97
103
 
98
104
  def finish(callback)
99
105
  process_line(:eof, callback)
100
- File.delete(@file.path) if @context[:delete_resource]
101
106
  end
102
107
  end
103
108
  end
@@ -1,19 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EXEL
2
4
  module Providers
3
5
  # The default remote provider. Doesn't actually upload and download files to and from remote storage, but rather
4
6
  # just works with local files.
5
7
  class LocalFileProvider
6
8
  def upload(file)
7
- "file://#{file.path}"
9
+ RemoteValue.new(URI("file://#{File.absolute_path(file)}"))
8
10
  end
9
11
 
10
- def download(uri)
11
- raise 'URI must begin with "file://"' unless uri.start_with? 'file://'
12
- File.open(uri.split('file://').last)
12
+ def download(remote_value)
13
+ scheme = remote_value.uri.scheme
14
+ raise "Unsupported URI scheme '#{scheme}'" unless scheme == 'file'
15
+ File.open(remote_value.uri.path)
13
16
  end
14
17
 
15
- def self.remote?(uri)
16
- uri =~ %r{file://}
18
+ def self.remote?(value)
19
+ value.is_a?(RemoteValue)
17
20
  end
18
21
  end
19
22
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EXEL
2
4
  module Providers
3
5
  # The default remote provider. Provides async execution by running the given EXEL block in a new Thread
@@ -0,0 +1,11 @@
1
+ class RemoteValue
2
+ attr_reader :uri
3
+
4
+ def initialize(uri)
5
+ @uri = uri
6
+ end
7
+
8
+ def ==(other)
9
+ other.class == self.class && other.uri == @uri
10
+ end
11
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative './ast_node'
2
4
 
3
5
  module EXEL
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EXEL
2
4
  # Contains methods to handle remote and local values. Used for {Context} serialization
3
5
  module Value
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EXEL
2
- VERSION = '1.2.1'.freeze
4
+ VERSION = '1.5.2'
3
5
  end
@@ -1,40 +1,61 @@
1
- module EXEL
2
- describe ASTNode do
3
- let(:context) { instance_double(EXEL::Context) }
1
+ # frozen_string_literal: true
4
2
 
5
- def instruction
6
- instance_double(Instruction, execute: nil)
7
- end
3
+ describe EXEL::ASTNode do
4
+ let(:context) { instance_double(EXEL::Context) }
5
+
6
+ def instruction
7
+ instance_double(EXEL::Instruction, execute: nil)
8
+ end
9
+
10
+ class TestNode < EXEL::ASTNode
11
+ end
8
12
 
9
- TestNode = Class.new(ASTNode)
13
+ describe '#start' do
14
+ context 'when a JobTermination error bubbles up' do
15
+ let(:node) { TestNode.new(instruction) }
16
+
17
+ before do
18
+ allow(node).to receive(:run).and_raise(EXEL::Error::JobTermination, 'Error')
19
+ end
20
+
21
+ it 'ensures the process fails silently' do
22
+ expect(EXEL.logger).to receive(:error).with('JobTerminationError: Error')
23
+ expect { node.start(context) }.not_to raise_error
24
+ end
10
25
 
11
- describe '#start' do
12
- context 'when an JobTermination error bubbles up' do
13
- it 'ensures the process fails silently' do
14
- node = TestNode.new(instruction)
15
- allow(node).to receive(:run).and_raise(EXEL::Error::JobTermination, 'Error')
16
- expect(EXEL.logger).to receive(:error).with('JobTerminationError: Error')
17
- expect { node.start(context) }.not_to raise_error
26
+ it 'logs the error by default' do
27
+ expect(EXEL.logger).to receive(:error).with('JobTerminationError: Error')
28
+ node.start(context)
29
+ end
30
+
31
+ context 'given a log instruction' do
32
+ before do
33
+ allow(node).to receive(:run).and_raise(EXEL::Error::JobTermination.new('Error', :warn))
34
+ end
35
+
36
+ it 'logs the error with the given cmd' do
37
+ expect(EXEL.logger).to receive(:warn).with('JobTerminationError: Error')
38
+ node.start(context)
18
39
  end
19
40
  end
20
41
  end
42
+ end
21
43
 
22
- describe '#run' do
23
- it 'raises an error if not implemented' do
24
- expect { TestNode.new(instruction).run(context) }.to raise_error 'EXEL::TestNode does not implement #process'
25
- end
44
+ describe '#run' do
45
+ it 'raises an error if not implemented' do
46
+ expect { TestNode.new(instruction).run(context) }.to raise_error 'TestNode does not implement #process'
26
47
  end
48
+ end
27
49
 
28
- describe '#add_child' do
29
- it 'adds the given node to its children' do
30
- root = ASTNode.new(instruction)
31
- child_node = ASTNode.new(instruction)
32
- child_node2 = ASTNode.new(instruction)
33
- root.add_child(child_node)
34
- root.add_child(child_node2)
50
+ describe '#add_child' do
51
+ it 'adds the given node to its children' do
52
+ root = EXEL::ASTNode.new(instruction)
53
+ child_node = EXEL::ASTNode.new(instruction)
54
+ child_node2 = EXEL::ASTNode.new(instruction)
55
+ root.add_child(child_node)
56
+ root.add_child(child_node2)
35
57
 
36
- expect(root.children).to eq([child_node, child_node2])
37
- end
58
+ expect(root.children).to eq([child_node, child_node2])
38
59
  end
39
60
  end
40
61
  end
@@ -1,108 +1,108 @@
1
- module EXEL
2
- describe Context do
3
- subject(:context) { EXEL::Context.new(key1: '1', key2: 2) }
1
+ # frozen_string_literal: true
4
2
 
5
- it { is_expected.to be_a(Hash) }
3
+ describe EXEL::Context do
4
+ subject(:context) { EXEL::Context.new(key1: '1', key2: 2) }
6
5
 
7
- describe '#initialize' do
8
- it 'initializes with a hash' do
9
- expect(context[:key1]).to eq('1')
10
- expect(context[:key2]).to eq(2)
11
- expect(context[:key3]).to be_nil
12
- end
6
+ it { is_expected.to be_a(Hash) }
7
+
8
+ describe '#initialize' do
9
+ it 'initializes with a hash' do
10
+ expect(context[:key1]).to eq('1')
11
+ expect(context[:key2]).to eq(2)
12
+ expect(context[:key3]).to be_nil
13
13
  end
14
+ end
14
15
 
15
- describe '#deep_dup' do
16
- it 'returns a deep copy of itself' do
17
- context[:a] = {nested: []}
16
+ describe '#deep_dup' do
17
+ it 'returns a deep copy of itself' do
18
+ context[:a] = {nested: []}
18
19
 
19
- dup = context.deep_dup
20
- expect(context).to eq(dup)
21
- expect(context).not_to be_equal(dup)
20
+ dup = context.deep_dup
21
+ expect(context).to eq(dup)
22
+ expect(context).not_to be_equal(dup)
22
23
 
23
- dup[:a][:nested] << 1
24
- expect(context[:a][:nested]).to be_empty
25
- end
24
+ dup[:a][:nested] << 1
25
+ expect(context[:a][:nested]).to be_empty
26
26
  end
27
+ end
27
28
 
28
- describe '#serialize' do
29
- before { allow(Value).to receive(:upload) }
30
-
31
- it 'writes the serialized context to a file and upload it' do
32
- expect(Value).to receive(:remotize).with(context[:key1]).and_return('remote_value1')
33
- expect(Value).to receive(:remotize).with(context[:key2]).and_return('remote_value2')
29
+ describe '#serialize' do
30
+ before { allow(EXEL::Value).to receive(:upload) }
34
31
 
35
- expect(SecureRandom).to receive(:uuid).and_return('uuid')
32
+ it 'writes the serialized context to a file and upload it' do
33
+ expect(EXEL::Value).to receive(:remotize).with(context[:key1]).and_return('remote_value1')
34
+ expect(EXEL::Value).to receive(:remotize).with(context[:key2]).and_return('remote_value2')
36
35
 
37
- expect(Value).to receive(:remotize) do |file|
38
- expect(file.read).to eq(Marshal.dump(Context.new(key1: 'remote_value1', key2: 'remote_value2')))
39
- expect(file.path).to include('uuid')
40
- 'file_uri'
41
- end
36
+ expect(SecureRandom).to receive(:uuid).and_return('uuid')
42
37
 
43
- expect(context.serialize).to eq('file_uri')
38
+ expect(EXEL::Value).to receive(:remotize) do |file|
39
+ expect(file.read).to eq(Marshal.dump(EXEL::Context.new(key1: 'remote_value1', key2: 'remote_value2')))
40
+ expect(file.path).to include('uuid')
41
+ 'file_uri'
44
42
  end
45
43
 
46
- it 'does not mutate the current context' do
47
- allow(Value).to receive(:remotize).and_return('remote_value')
48
- original_table = context.dup
49
- context.serialize
50
- expect(context).to eq(original_table)
51
- end
44
+ expect(context.serialize).to eq('file_uri')
52
45
  end
53
46
 
54
- describe '.deserialize' do
55
- it 'deserializes a given uri' do
56
- file = StringIO.new(Marshal.dump(context))
57
- expect(Value).to receive(:localize).with('uri').and_return(file)
47
+ it 'does not mutate the current context' do
48
+ allow(EXEL::Value).to receive(:remotize).and_return('remote_value')
49
+ original_table = context.dup
50
+ context.serialize
51
+ expect(context).to eq(original_table)
52
+ end
53
+ end
58
54
 
59
- expect(Context.deserialize('uri')).to eq(context)
55
+ describe '.deserialize' do
56
+ it 'deserializes a given uri' do
57
+ file = StringIO.new(Marshal.dump(context))
58
+ expect(EXEL::Value).to receive(:localize).with('uri').and_return(file)
60
59
 
61
- expect(file).to be_closed
62
- end
60
+ expect(EXEL::Context.deserialize('uri')).to eq(context)
61
+
62
+ expect(file).to be_closed
63
63
  end
64
+ end
64
65
 
65
- shared_examples 'a reader method' do
66
- subject(:context) { EXEL::Context.new(key: 'value') }
66
+ shared_examples 'a reader method' do
67
+ subject(:context) { EXEL::Context.new(key: 'value') }
67
68
 
68
- it 'returns the value' do
69
- expect(context.send(method, :key)).to eq('value')
70
- end
69
+ it 'returns the value' do
70
+ expect(context.send(method, :key)).to eq('value')
71
+ end
71
72
 
72
- it 'localizes the returned value' do
73
- expect(Value).to receive(:localize).with('value').and_return('localized')
74
- expect(context.send(method, :key)).to eq('localized')
75
- end
73
+ it 'localizes the returned value' do
74
+ expect(EXEL::Value).to receive(:localize).with('value').and_return('localized')
75
+ expect(context.send(method, :key)).to eq('localized')
76
+ end
76
77
 
77
- it 'stores the localized value' do
78
- allow(Value).to receive(:localize).with('value').and_return('localized')
79
- context.send(method, :key)
80
- allow(Value).to receive(:localize).with('localized').and_return('localized')
81
- context.send(method, :key)
82
- end
78
+ it 'stores the localized value' do
79
+ allow(EXEL::Value).to receive(:localize).with('value').and_return('localized')
80
+ context.send(method, :key)
81
+ allow(EXEL::Value).to receive(:localize).with('localized').and_return('localized')
82
+ context.send(method, :key)
83
+ end
83
84
 
84
- it 'looks up deferred values' do
85
- # eq(context) as an argument matcher is necessary to prevent RSpec from calling fetch on the context, leading to
86
- # a stack overflow
87
- expect(DeferredContextValue).to receive(:resolve).with('value', eq(context)).and_return('resolved')
88
- expect(context.send(method, :key)).to eq('resolved')
89
- end
85
+ it 'looks up deferred values' do
86
+ # eq(context) as an argument matcher is necessary to prevent RSpec from calling fetch on the context, leading to
87
+ # a stack overflow
88
+ expect(EXEL::DeferredContextValue).to receive(:resolve).with('value', eq(context)).and_return('resolved')
89
+ expect(context.send(method, :key)).to eq('resolved')
90
90
  end
91
+ end
91
92
 
92
- describe '#[]' do
93
- it_behaves_like 'a reader method' do
94
- let(:method) { :[] }
95
- end
93
+ describe '#[]' do
94
+ it_behaves_like 'a reader method' do
95
+ let(:method) { :[] }
96
96
  end
97
+ end
97
98
 
98
- describe '#fetch' do
99
- it 'raises an exception if the key is not found' do
100
- expect { context.fetch(:unknown) }.to raise_error(KeyError)
101
- end
99
+ describe '#fetch' do
100
+ it 'raises an exception if the key is not found' do
101
+ expect { context.fetch(:unknown) }.to raise_error(KeyError)
102
+ end
102
103
 
103
- it_behaves_like 'a reader method' do
104
- let(:method) { :fetch }
105
- end
104
+ it_behaves_like 'a reader method' do
105
+ let(:method) { :fetch }
106
106
  end
107
107
  end
108
108
  end