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.
- checksums.yaml +5 -5
- data/.codeclimate.yml +4 -4
- data/.gitignore +1 -2
- data/.rubocop.yml +23 -14
- data/.rubocop_airbnb.yml +2 -0
- data/.rubocop_todo.yml +1 -13
- data/.travis.yml +26 -0
- data/Gemfile +2 -2
- data/Gemfile.lock +118 -0
- data/Guardfile +1 -0
- data/README.md +96 -31
- data/Rakefile +2 -0
- data/exel.gemspec +7 -7
- data/lib/exel.rb +7 -1
- data/lib/exel/ast_node.rb +6 -10
- data/lib/exel/context.rb +4 -1
- data/lib/exel/deferred_context_value.rb +3 -1
- data/lib/exel/error/job_termination.rb +12 -0
- data/lib/exel/events.rb +6 -0
- data/lib/exel/instruction.rb +5 -2
- data/lib/exel/instruction_node.rb +2 -0
- data/lib/exel/job.rb +8 -4
- data/lib/exel/listen_instruction.rb +2 -0
- data/lib/exel/logging.rb +24 -1
- data/lib/exel/logging/logger_wrapper.rb +31 -0
- data/lib/exel/logging_helper.rb +36 -0
- data/lib/exel/middleware/chain.rb +67 -0
- data/lib/exel/middleware/logging.rb +30 -0
- data/lib/exel/null_instruction.rb +2 -0
- data/lib/exel/processor_helper.rb +9 -1
- data/lib/exel/processors/async_processor.rb +2 -8
- data/lib/exel/processors/run_processor.rb +2 -6
- data/lib/exel/processors/split_processor.rb +15 -10
- data/lib/exel/providers/local_file_provider.rb +9 -6
- data/lib/exel/providers/threaded_async_provider.rb +2 -0
- data/lib/exel/remote_value.rb +11 -0
- data/lib/exel/sequence_node.rb +2 -0
- data/lib/exel/value.rb +2 -0
- data/lib/exel/version.rb +3 -1
- data/spec/exel/ast_node_spec.rb +48 -27
- data/spec/exel/context_spec.rb +77 -77
- data/spec/exel/deferred_context_value_spec.rb +42 -42
- data/spec/exel/events_spec.rb +68 -59
- data/spec/exel/instruction_node_spec.rb +17 -16
- data/spec/exel/instruction_spec.rb +49 -42
- data/spec/exel/job_spec.rb +99 -84
- data/spec/exel/listen_instruction_spec.rb +11 -10
- data/spec/exel/logging/logger_wrapper_spec.rb +93 -0
- data/spec/exel/logging_helper_spec.rb +24 -0
- data/spec/exel/logging_spec.rb +69 -24
- data/spec/exel/middleware/chain_spec.rb +65 -0
- data/spec/exel/middleware/logging_spec.rb +31 -0
- data/spec/exel/middleware_spec.rb +68 -0
- data/spec/exel/null_instruction_spec.rb +4 -4
- data/spec/exel/processors/async_processor_spec.rb +17 -18
- data/spec/exel/processors/run_processor_spec.rb +10 -11
- data/spec/exel/processors/split_processor_spec.rb +99 -74
- data/spec/exel/providers/local_file_provider_spec.rb +26 -28
- data/spec/exel/providers/threaded_async_provider_spec.rb +37 -38
- data/spec/exel/sequence_node_spec.rb +12 -11
- data/spec/exel/value_spec.rb +33 -33
- data/spec/exel_spec.rb +9 -7
- data/spec/integration/integration_spec.rb +3 -1
- data/spec/spec_helper.rb +4 -2
- data/spec/support/integration_test_classes.rb +4 -3
- metadata +37 -48
@@ -1,24 +1,18 @@
|
|
1
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 '../
|
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::
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
9
|
+
RemoteValue.new(URI("file://#{File.absolute_path(file)}"))
|
8
10
|
end
|
9
11
|
|
10
|
-
def download(
|
11
|
-
|
12
|
-
|
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?(
|
16
|
-
|
18
|
+
def self.remote?(value)
|
19
|
+
value.is_a?(RemoteValue)
|
17
20
|
end
|
18
21
|
end
|
19
22
|
end
|
data/lib/exel/sequence_node.rb
CHANGED
data/lib/exel/value.rb
CHANGED
data/lib/exel/version.rb
CHANGED
data/spec/exel/ast_node_spec.rb
CHANGED
@@ -1,40 +1,61 @@
|
|
1
|
-
|
2
|
-
describe ASTNode do
|
3
|
-
let(:context) { instance_double(EXEL::Context) }
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
37
|
-
end
|
58
|
+
expect(root.children).to eq([child_node, child_node2])
|
38
59
|
end
|
39
60
|
end
|
40
61
|
end
|
data/spec/exel/context_spec.rb
CHANGED
@@ -1,108 +1,108 @@
|
|
1
|
-
|
2
|
-
describe Context do
|
3
|
-
subject(:context) { EXEL::Context.new(key1: '1', key2: 2) }
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
|
-
|
3
|
+
describe EXEL::Context do
|
4
|
+
subject(:context) { EXEL::Context.new(key1: '1', key2: 2) }
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
16
|
+
describe '#deep_dup' do
|
17
|
+
it 'returns a deep copy of itself' do
|
18
|
+
context[:a] = {nested: []}
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
dup = context.deep_dup
|
21
|
+
expect(context).to eq(dup)
|
22
|
+
expect(context).not_to be_equal(dup)
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
end
|
24
|
+
dup[:a][:nested] << 1
|
25
|
+
expect(context[:a][:nested]).to be_empty
|
26
26
|
end
|
27
|
+
end
|
27
28
|
|
28
|
-
|
29
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
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
|
-
|
62
|
-
|
60
|
+
expect(EXEL::Context.deserialize('uri')).to eq(context)
|
61
|
+
|
62
|
+
expect(file).to be_closed
|
63
63
|
end
|
64
|
+
end
|
64
65
|
|
65
|
-
|
66
|
-
|
66
|
+
shared_examples 'a reader method' do
|
67
|
+
subject(:context) { EXEL::Context.new(key: 'value') }
|
67
68
|
|
68
|
-
|
69
|
-
|
70
|
-
|
69
|
+
it 'returns the value' do
|
70
|
+
expect(context.send(method, :key)).to eq('value')
|
71
|
+
end
|
71
72
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
end
|
93
|
+
describe '#[]' do
|
94
|
+
it_behaves_like 'a reader method' do
|
95
|
+
let(:method) { :[] }
|
96
96
|
end
|
97
|
+
end
|
97
98
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
104
|
-
|
105
|
-
end
|
104
|
+
it_behaves_like 'a reader method' do
|
105
|
+
let(:method) { :fetch }
|
106
106
|
end
|
107
107
|
end
|
108
108
|
end
|