drudgery 0.0.2 → 0.0.3

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.
data/README.md CHANGED
@@ -55,7 +55,7 @@ m = Drudgery::Manager.new
55
55
  m.prepare do |job|
56
56
  job.extract :csv, 'src/addresses.csv'
57
57
 
58
- job.transform do |data|
58
+ job.transform do |data, cache|
59
59
  first_name, last_name = data.delete(:name).split(' ')
60
60
 
61
61
  data[:first_name] = first_name
@@ -93,7 +93,7 @@ m.prepare do |job|
93
93
  extractor.order('name')
94
94
  end
95
95
 
96
- job.transform do |data|
96
+ job.transform do |data, cache|
97
97
  first_name, last_name = data.delete(:name).split(' ')
98
98
 
99
99
  data[:first_name] = first_name
@@ -132,9 +132,9 @@ end
132
132
  source = []
133
133
 
134
134
  m = Drudgery::Manager.new
135
- job = Drudgery::Job.new(:extractor => ArrayExtractor.new(source))
136
135
 
137
- m.prepare(job) do |job|
136
+ m.prepare do |job|
137
+ m.extract ArrayExtractor.new(source)
138
138
  m.load :csv, 'destination.csv'
139
139
  end
140
140
  ```
@@ -173,12 +173,12 @@ Transformers
173
173
  ------------
174
174
 
175
175
  Drudgery comes with a basic Transformer class. It symbolizes the keys of
176
- each record and allows you to register processors to process data. Registered
177
- processors should implement a `#call` method and return a `Hash` or `nil`.
176
+ each record and allows you to register a processor to process data. The
177
+ processor should implement a `#call` method and return a `Hash` or `nil`.
178
178
 
179
179
  ```ruby
180
180
  custom_processor = Proc.new do |data, cache|
181
- data[:initials] = data[:name].split(' ').map(&:capitalize)
181
+ data[:initials] = data[:name].split(' ').map(&:capitalize).join()
182
182
  data
183
183
  end
184
184
 
@@ -190,7 +190,7 @@ transformer.transform({ :name => 'John Doe' }) # == { :name => 'John Doe', :init
190
190
 
191
191
  You could also implement your own transformer if you need more custom
192
192
  processing power. If you inherit from `Drudgery::Transfomer`, you need
193
- only implement the `#transform` method that accepts a hash as an
193
+ only implement the `#transform` method that accepts a hash argument as an
194
194
  argument and returns a `Hash` or `nil`.
195
195
 
196
196
  ```ruby
@@ -201,10 +201,10 @@ class CustomTransformer < Drudgery::Transformer
201
201
  end
202
202
 
203
203
  m = Drudgery::Manager.new
204
- job = Drudgery::Job.new(:transformer => CustomTransformer.new)
205
204
 
206
- m.prepare(job) do |job|
205
+ m.prepare do |job|
207
206
  m.extract :csv, 'source.csv'
207
+ m.transform( CustomTransformer.new)
208
208
  m.load :csv, 'destination.csv'
209
209
  end
210
210
  ```
@@ -237,10 +237,10 @@ end
237
237
  destination = []
238
238
 
239
239
  m = Drudgery::Manager.new
240
- job = Drudgery::Job.new(:loader => ArrayLoader.new(destination))
241
240
 
242
- m.prepare(job) do |job|
241
+ m.prepare do |job|
243
242
  m.extract :csv, 'source.csv'
243
+ m.load ArrayLoader.new(destination)
244
244
  end
245
245
  ```
246
246
 
data/lib/drudgery/job.rb CHANGED
@@ -3,29 +3,40 @@ module Drudgery
3
3
  def initialize(options={})
4
4
  @extractor = options[:extractor]
5
5
  @loader = options[:loader]
6
- @transformer = options[:transformer] || Drudgery::Transformer.new
6
+ @transformer = options[:transformer]
7
+ @batch_size = options[:batch_size] || 1000
7
8
 
8
- @batch_size, @records = 1000, []
9
+ @records = []
9
10
  end
10
11
 
11
12
  def batch_size(size)
12
13
  @batch_size = size
13
14
  end
14
15
 
15
- def extract(type, *args)
16
- extractor = Drudgery::Extractors.instantiate(type, *args)
16
+ def extract(*args)
17
+ if args.first.kind_of?(Symbol)
18
+ extractor = Drudgery::Extractors.instantiate(*args)
19
+ else
20
+ extractor = args.first
21
+ end
17
22
 
18
23
  yield extractor if block_given?
19
24
 
20
25
  @extractor = extractor
21
26
  end
22
27
 
23
- def transform(&processor)
24
- @transformer.register(processor)
28
+ def transform(transformer=Drudgery::Transformer.new, &processor)
29
+ transformer.register(processor)
30
+
31
+ @transformer = transformer
25
32
  end
26
33
 
27
- def load(type, *args)
28
- loader = Drudgery::Loaders.instantiate(type, *args)
34
+ def load(*args)
35
+ if args.first.kind_of?(Symbol)
36
+ loader = Drudgery::Loaders.instantiate(*args)
37
+ else
38
+ loader = args.first
39
+ end
29
40
 
30
41
  yield loader if block_given?
31
42
 
@@ -47,7 +58,7 @@ module Drudgery
47
58
  private
48
59
  def extract_records
49
60
  @extractor.extract do |data|
50
- record = @transformer.transform(data)
61
+ record = transform_data(data)
51
62
  next if record.nil?
52
63
 
53
64
  yield record
@@ -58,5 +69,13 @@ module Drudgery
58
69
  @loader.load(@records)
59
70
  @records.clear
60
71
  end
72
+
73
+ def transform_data(data)
74
+ if @transformer
75
+ @transformer.transform(data)
76
+ else
77
+ data
78
+ end
79
+ end
61
80
  end
62
81
  end
@@ -1,23 +1,17 @@
1
1
  module Drudgery
2
2
  class Transformer
3
3
  def initialize
4
- @processors = []
5
4
  @cache = {}
6
5
  end
7
6
 
8
7
  def register(processor)
9
- @processors << processor
8
+ @processor = processor
10
9
  end
11
10
 
12
11
  def transform(data)
13
12
  symbolize_keys!(data)
14
13
 
15
- @processors.each do |processor|
16
- data = processor.call(data, @cache)
17
- break if data.nil?
18
- end
19
-
20
- data
14
+ @processor ? @processor.call(data, @cache) : data
21
15
  end
22
16
 
23
17
  private
@@ -1,3 +1,3 @@
1
1
  module Drudgery
2
- VERSION = '0.0.2'
2
+ VERSION = '0.0.3'
3
3
  end
@@ -16,10 +16,15 @@ describe Drudgery::Job do
16
16
  @job.instance_variable_get('@loader').must_equal @loader
17
17
  end
18
18
 
19
+ it 'sets batch_size with provided argument' do
20
+ job = Drudgery::Job.new(:batch_size => 100)
21
+ job.instance_variable_get('@batch_size').must_equal(100)
22
+ end
23
+
19
24
  it 'initializes extractor, transformer, and loader if none provided' do
20
25
  job = Drudgery::Job.new
21
26
  job.instance_variable_get('@extractor').must_be_nil
22
- job.instance_variable_get('@transformer').must_be_instance_of(Drudgery::Transformer)
27
+ job.instance_variable_get('@transformer').must_be_nil
23
28
  job.instance_variable_get('@loader').must_be_nil
24
29
  end
25
30
 
@@ -27,7 +32,8 @@ describe Drudgery::Job do
27
32
  @job.instance_variable_get('@records').must_equal []
28
33
  end
29
34
 
30
- it 'initializes batch_size as 1000' do
35
+
36
+ it 'initializes batch_size as 1000 if none provided' do
31
37
  @job.instance_variable_get('@batch_size').must_equal 1000
32
38
  end
33
39
  end
@@ -41,85 +47,196 @@ describe Drudgery::Job do
41
47
  end
42
48
 
43
49
  describe '#extract' do
44
- it 'instantiates extractor with type and args' do
45
- Drudgery::Extractors.expects(:instantiate).with(:csv, 'filename.csv', :col_sep => '|')
50
+ describe 'when type and args provided' do
51
+ it 'instantiates extractor with type and args' do
52
+ Drudgery::Extractors.expects(:instantiate).with(:csv, 'filename.csv', :col_sep => '|')
46
53
 
47
- job = Drudgery::Job.new
48
- job.extract(:csv, 'filename.csv', :col_sep => '|')
49
- end
54
+ job = Drudgery::Job.new
55
+ job.extract(:csv, 'filename.csv', :col_sep => '|')
56
+ end
50
57
 
51
- it 'yields extractor if block_given' do
52
- extractor = mock
53
- extractor.expects(:col_sep).with('|')
58
+ it 'yields extractor if block_given' do
59
+ extractor = mock
60
+ extractor.expects(:col_sep).with('|')
54
61
 
55
- Drudgery::Extractors.stubs(:instantiate).returns(extractor)
62
+ Drudgery::Extractors.stubs(:instantiate).returns(extractor)
56
63
 
57
- job = Drudgery::Job.new
58
- job.extract(:csv, 'filename.csv') do |extractor|
59
- extractor.col_sep '|'
64
+ job = Drudgery::Job.new
65
+ job.extract(:csv, 'filename.csv') do |extractor|
66
+ extractor.col_sep '|'
67
+ end
68
+ end
69
+
70
+ it 'sets extractor' do
71
+ extractor = mock
72
+
73
+ Drudgery::Extractors.stubs(:instantiate).returns(extractor)
74
+
75
+ job = Drudgery::Job.new
76
+ job.extract(:csv, 'filename.csv', :col_sep => '|')
77
+
78
+ job.instance_variable_get('@extractor').must_equal extractor
60
79
  end
61
80
  end
62
81
 
63
- it 'sets extractor' do
64
- extractor = mock
82
+ describe 'when extractor provided' do
83
+ it 'does not instantiat extractor with type and args' do
84
+ extractor = mock
65
85
 
66
- Drudgery::Extractors.stubs(:instantiate).returns(extractor)
86
+ Drudgery::Extractors.expects(:instantiate).never
67
87
 
68
- job = Drudgery::Job.new
69
- job.extract(:csv, 'filename.csv', :col_sep => '|')
88
+ job = Drudgery::Job.new
89
+ job.extract(extractor)
90
+ end
91
+
92
+ it 'yields extractor if block_given' do
93
+ extractor = mock
94
+ extractor.expects(:col_sep).with('|')
95
+
96
+ job = Drudgery::Job.new
97
+ job.extract(extractor) do |ext|
98
+ ext.col_sep '|'
99
+ end
100
+ end
70
101
 
71
- job.instance_variable_get('@extractor').must_equal extractor
102
+ it 'sets extractor' do
103
+ extractor = mock
104
+
105
+ job = Drudgery::Job.new
106
+ job.extract(extractor)
107
+
108
+ job.instance_variable_get('@extractor').must_equal extractor
109
+ end
72
110
  end
73
111
  end
74
112
 
75
113
  describe '#transform' do
76
- it 'registers provided proc with transformer' do
77
- block = Proc.new { |data, cache| data }
114
+ describe 'when transformer provided' do
115
+ it 'sets transformer to provided transformer' do
116
+ transformer = mock
117
+ transformer.stubs(:register)
78
118
 
79
- transformer = mock
80
- transformer.expects(:register).with(block)
119
+ job = Drudgery::Job.new
120
+ job.transform(transformer)
121
+
122
+ job.instance_variable_get('@transformer').must_equal transformer
123
+ end
124
+
125
+ it 'registers provided proc with provided transformer' do
126
+ block = Proc.new { |data, cache| data }
127
+
128
+ transformer = mock
129
+ transformer.expects(:register).with(block)
130
+
131
+ job = Drudgery::Job.new
132
+ job.transform(transformer, &block)
133
+ end
81
134
 
82
- job = Drudgery::Job.new(:transformer => transformer)
83
- job.transform(&block)
135
+ it 'registers provided block with provided transformer' do
136
+ transformer = mock
137
+ transformer.expects(:register).with { |data, cache| data }
138
+
139
+ job = Drudgery::Job.new
140
+ job.transform(transformer) { |data, cache| data }
141
+ end
84
142
  end
85
143
 
86
- it 'registers provided block with transformer' do
87
- transformer = mock
88
- transformer.expects(:register).with { |data, cache| data }
144
+ describe 'when no transformer provided' do
145
+ it 'sets transformer to default transformer' do
146
+ transformer = mock
147
+ transformer.stubs(:register)
148
+
149
+ Drudgery::Transformer.expects(:new).returns(transformer)
150
+
151
+ job = Drudgery::Job.new
152
+ job.transform
153
+
154
+ job.instance_variable_get('@transformer').must_equal transformer
155
+ end
156
+
157
+ it 'registers provided proc with default transformer' do
158
+ block = Proc.new { |data, cache| data }
159
+
160
+ transformer = mock
161
+ transformer.expects(:register).with(block)
162
+
163
+ Drudgery::Transformer.stubs(:new).returns(transformer)
164
+
165
+ job = Drudgery::Job.new
166
+ job.transform(&block)
167
+ end
89
168
 
90
- job = Drudgery::Job.new(:transformer => transformer)
91
- job.transform { |data, cache| data }
169
+ it 'registers provided block with default transformer' do
170
+ transformer = Drudgery::Transformer.new
171
+ transformer.expects(:register).with { |data, cache| data }
172
+
173
+ Drudgery::Transformer.stubs(:new).returns(transformer)
174
+
175
+ job = Drudgery::Job.new
176
+ job.transform { |data, cache| data }
177
+ end
92
178
  end
93
179
  end
94
180
 
95
181
  describe '#load' do
96
- it 'instantiates loader with type with args' do
97
- Drudgery::Loaders.expects(:instantiate).with(:sqlite3, 'db.sqlite3', 'tablename')
182
+ describe 'when type and args provided' do
183
+ it 'instantiates loader with type with args' do
184
+ Drudgery::Loaders.expects(:instantiate).with(:sqlite3, 'db.sqlite3', 'tablename')
98
185
 
99
- job = Drudgery::Job.new
100
- job.load(:sqlite3, 'db.sqlite3', 'tablename')
101
- end
186
+ job = Drudgery::Job.new
187
+ job.load(:sqlite3, 'db.sqlite3', 'tablename')
188
+ end
102
189
 
103
- it 'yields loader if block_given' do
104
- loader = mock
105
- loader.expects(:select).with('a', 'b', 'c')
190
+ it 'yields loader if block_given' do
191
+ loader = mock
192
+ loader.expects(:select).with('a', 'b', 'c')
106
193
 
107
- Drudgery::Loaders.stubs(:instantiate).with(:sqlite3, 'db.sqlite3', 'tablename').returns(loader)
194
+ Drudgery::Loaders.stubs(:instantiate).with(:sqlite3, 'db.sqlite3', 'tablename').returns(loader)
108
195
 
109
- job = Drudgery::Job.new
110
- job.load(:sqlite3, 'db.sqlite3', 'tablename') do |loader|
111
- loader.select('a', 'b', 'c')
196
+ job = Drudgery::Job.new
197
+ job.load(:sqlite3, 'db.sqlite3', 'tablename') do |loader|
198
+ loader.select('a', 'b', 'c')
199
+ end
200
+ end
201
+
202
+ it 'sets loader' do
203
+ loader = mock
204
+
205
+ Drudgery::Loaders.expects(:instantiate).with(:sqlite3, 'db.sqlite3', 'tablename').returns(loader)
206
+
207
+ job = Drudgery::Job.new
208
+ job.load(:sqlite3, 'db.sqlite3', 'tablename')
209
+ job.instance_variable_get('@loader').must_equal loader
112
210
  end
113
211
  end
114
212
 
115
- it 'sets extractor' do
116
- loader = mock
213
+ describe 'when loader provided' do
214
+ it 'does not instantiate loader with type with args' do
215
+ loader = mock
117
216
 
118
- Drudgery::Loaders.expects(:instantiate).with(:sqlite3, 'db.sqlite3', 'tablename').returns(loader)
217
+ Drudgery::Loaders.expects(:instantiate).never
119
218
 
120
- job = Drudgery::Job.new
121
- job.load(:sqlite3, 'db.sqlite3', 'tablename')
122
- job.instance_variable_get('@loader').must_equal loader
219
+ job = Drudgery::Job.new
220
+ job.load(loader)
221
+ end
222
+
223
+ it 'yields loader if block_given' do
224
+ loader = mock
225
+ loader.expects(:select).with('a', 'b', 'c')
226
+
227
+ job = Drudgery::Job.new
228
+ job.load(loader) do |loader|
229
+ loader.select('a', 'b', 'c')
230
+ end
231
+ end
232
+
233
+ it 'sets loader' do
234
+ loader = mock
235
+
236
+ job = Drudgery::Job.new
237
+ job.load(loader)
238
+ job.instance_variable_get('@loader').must_equal loader
239
+ end
123
240
  end
124
241
  end
125
242
 
@@ -156,8 +273,8 @@ describe Drudgery::Job do
156
273
  extractor.stubs(:extract).multiple_yields([{ 'a' => 1 }], [{ 'b' => 2 }], [{ 'c' => 3 }])
157
274
 
158
275
  loader = mock
159
- loader.expects(:load).with([{ :a => 1 }, { :b => 2 }])
160
- loader.expects(:load).with([{ :c => 3 }])
276
+ loader.expects(:load).with([{ 'a' => 1 }, { 'b' => 2 }])
277
+ loader.expects(:load).with([{ 'c' => 3 }])
161
278
 
162
279
  job = Drudgery::Job.new(:extractor => extractor, :loader => loader)
163
280
  job.batch_size 2
@@ -6,21 +6,19 @@ describe Drudgery::Transformer do
6
6
  end
7
7
 
8
8
  describe '#initialize' do
9
- it 'initializes processors array' do
10
- @transformer.instance_variable_get('@processors').must_equal []
11
- end
12
-
13
9
  it 'initializes cache hash' do
14
10
  @transformer.instance_variable_get('@cache').must_equal({})
15
11
  end
16
12
  end
17
13
 
18
14
  describe '#register' do
19
- it 'adds processor to processors array' do
15
+ it 'sets processor with provided proc' do
20
16
  processor = Proc.new { |data, cache| data }
21
17
 
22
18
  @transformer.register(processor)
23
- @transformer.instance_variable_get('@processors').must_include processor
19
+
20
+ # must_equal bug with comparing procs, so use assert_equal instead
21
+ assert_equal @transformer.instance_variable_get('@processor'), processor
24
22
  end
25
23
  end
26
24
 
@@ -29,14 +27,14 @@ describe Drudgery::Transformer do
29
27
  @transformer.transform({ 'a' => 1 }).must_equal({ :a => 1 })
30
28
  end
31
29
 
32
- it 'processes data in each processor' do
30
+ it 'processes data with processor' do
33
31
  processor = Proc.new { |data, cache| data[:b] = 2; data }
34
32
 
35
33
  @transformer.register(processor)
36
34
  @transformer.transform({ 'a' => 1 }).must_equal({ :a => 1, :b => 2 })
37
35
  end
38
36
 
39
- it 'allows processors to use cache' do
37
+ it 'allows processor to use cache' do
40
38
  processor = Proc.new do |data, cache|
41
39
  cache[:a] ||= 0
42
40
  cache[:a] += data[:a]
@@ -49,12 +47,10 @@ describe Drudgery::Transformer do
49
47
  @transformer.instance_variable_get('@cache').must_equal({ :a => 6 })
50
48
  end
51
49
 
52
- it 'skips remaining processors and returns nil if any processor returns nil' do
53
- processor1 = Proc.new { |data, cache| nil }
54
- processor2 = Proc.new { |data, cache| raise 'should not get here' }
50
+ it 'returns nil if any processor returns nil' do
51
+ processor = Proc.new { |data, cache| nil }
55
52
 
56
- @transformer.register(processor1)
57
- @transformer.register(processor2)
53
+ @transformer.register(processor)
58
54
  @transformer.transform({ 'a' => 1 }).must_be_nil
59
55
  end
60
56
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: drudgery
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-04-03 00:00:00.000000000 Z
12
+ date: 2012-04-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
16
- requirement: &70334397325460 !ruby/object:Gem::Requirement
16
+ requirement: &70230466813560 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *70334397325460
24
+ version_requirements: *70230466813560
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: bundler
27
- requirement: &70334397324780 !ruby/object:Gem::Requirement
27
+ requirement: &70230466812880 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '1.1'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70334397324780
35
+ version_requirements: *70230466812880
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: mocha
38
- requirement: &70334397324280 !ruby/object:Gem::Requirement
38
+ requirement: &70230466812240 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0.10'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70334397324280
46
+ version_requirements: *70230466812240
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: simplecov
49
- requirement: &70334397323700 !ruby/object:Gem::Requirement
49
+ requirement: &70230466811700 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0.6'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70334397323700
57
+ version_requirements: *70230466811700
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: guard-minitest
60
- requirement: &70334397323080 !ruby/object:Gem::Requirement
60
+ requirement: &70230466811240 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ~>
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: '0.5'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70334397323080
68
+ version_requirements: *70230466811240
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: activerecord
71
- requirement: &70334397322440 !ruby/object:Gem::Requirement
71
+ requirement: &70230466810720 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ~>
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '3.0'
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *70334397322440
79
+ version_requirements: *70230466810720
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: activerecord-import
82
- requirement: &70334397321700 !ruby/object:Gem::Requirement
82
+ requirement: &70230466810220 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ~>
@@ -87,10 +87,10 @@ dependencies:
87
87
  version: 0.2.9
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *70334397321700
90
+ version_requirements: *70230466810220
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: sqlite3
93
- requirement: &70334397321080 !ruby/object:Gem::Requirement
93
+ requirement: &70230466809620 !ruby/object:Gem::Requirement
94
94
  none: false
95
95
  requirements:
96
96
  - - ~>
@@ -98,7 +98,7 @@ dependencies:
98
98
  version: '1.3'
99
99
  type: :development
100
100
  prerelease: false
101
- version_requirements: *70334397321080
101
+ version_requirements: *70230466809620
102
102
  description: A simple ETL library that supports CSV, SQLite3, and ActiveRecord sources
103
103
  and destinations.
104
104
  email: