activewarehouse-etl 1.0.0.rc1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -4,4 +4,5 @@ test/output/*
4
4
  rdoc
5
5
  .rvmrc
6
6
  .bundle
7
- *.gem
7
+ *.gem
8
+ *.lock
data/CHANGELOG CHANGED
@@ -1,4 +1,9 @@
1
- 0.9.5 - unreleased
1
+ 1.0.0
2
+ * Fixes for Rails 3.2 (pgericson)
3
+ * Allow .ctl.rb and .ebf.rb extensions for easier syntax coloring (chrisgogreen)
4
+ * Support for limit/offset in database source (kennym)
5
+ * Support for error handlers in etl: can be declared with on_error { |error| } (thbar)
6
+ * Support for mysql streaming for better performance on large data sets - use :mysqlstream => true on database source (main code by pdodds, tests and fixes by sgrgic and thbar)
2
7
  * EnsureFieldsPresenceProcessor treat symbols and strings equally (thbar)
3
8
  * BREAKING CHANGE: you must require 'iconv' if you use the :encode_processor from now on (thbar)
4
9
  * A non-zero exit code should be returned on fatal screen or exceeded error threshold (thbar)
@@ -0,0 +1,6 @@
1
+ db = ENV['DB'] || 'mysql2'
2
+ gemfile = ENV['GEMFILE'] || 'test/config/gemfiles/Gemfile.rails-3.2.x'
3
+
4
+ guard 'shell' do
5
+ watch(/(lib|test)\/\.*/) {|m| `bundle exec rake ci:run_one[#{db},#{gemfile}]` }
6
+ end
@@ -55,7 +55,7 @@ ActiveWarehouse-ETL is the work of many people since late 2006 - here is a list,
55
55
  * Jeremy Lecour
56
56
  * Steve Meyfroidt
57
57
  * Seth Ladd
58
- * Thibaut Barrère
58
+ * "Thibaut Barrère":https://github.com/thbar
59
59
  * Stephen Touset
60
60
  * sasikumargn
61
61
  * Andrew Kuklewicz
@@ -64,6 +64,11 @@ ActiveWarehouse-ETL is the work of many people since late 2006 - here is a list,
64
64
  * Tyler Kiley
65
65
  * Colman Nady
66
66
  * Scott Gonyea
67
+ * "Philip Dodds":https://github.com/pdodds
68
+ * "Sinisa Grgic":https://github.com/sgrgic
69
+ * "Kenny Meyer":https://github.com/kennym
70
+ * "Chris":https://github.com/chrisgogreen
71
+ * "Peter Glerup Ericson":https://github.com/pgericson
67
72
 
68
73
  If your name should be on the list but isn't, please leave a comment!
69
74
 
@@ -25,6 +25,8 @@ Gem::Specification.new do |s|
25
25
  s.add_development_dependency('shoulda', '~>2.11.3')
26
26
  s.add_development_dependency('flexmock', '~>0.9.0')
27
27
  s.add_development_dependency('cartesian')
28
+ s.add_development_dependency('guard')
29
+ s.add_development_dependency('guard-shell')
28
30
 
29
31
  s.files = `git ls-files`.split("\n")
30
32
  s.test_files = `git ls-files -- {test}/*`.split("\n")
@@ -41,7 +41,12 @@ module ETL #:nodoc:
41
41
  def dependencies
42
42
  control.dependencies
43
43
  end
44
-
44
+
45
+ # Register an error handler
46
+ def on_error(&block)
47
+ control.error_handlers << block
48
+ end
49
+
45
50
  # Define a source.
46
51
  def source(name, configuration={}, definition={})
47
52
  if configuration[:type]
@@ -376,6 +381,11 @@ module ETL #:nodoc:
376
381
  :warn => []
377
382
  }
378
383
  end
384
+
385
+ # A array of Procs to be invoked for errors notifications
386
+ def error_handlers
387
+ @error_handlers ||= []
388
+ end
379
389
 
380
390
  # Get the error threshold. Defaults to 100.
381
391
  def error_threshold
@@ -1,6 +1,8 @@
1
1
  require 'fileutils'
2
2
 
3
3
  module ETL #:nodoc:
4
+ class NoLimitSpecifiedError < StandardError; end
5
+
4
6
  class Source < ::ActiveRecord::Base #:nodoc:
5
7
  # Connection for database sources
6
8
  end
@@ -189,12 +191,13 @@ module ETL #:nodoc:
189
191
 
190
192
  q << " GROUP BY #{group}" if group
191
193
  q << " ORDER BY #{order}" if order
192
-
193
- if ETL::Engine.limit || ETL::Engine.offset
194
- options = {}
195
- options[:limit] = ETL::Engine.limit if ETL::Engine.limit
196
- options[:offset] = ETL::Engine.offset if ETL::Engine.offset
197
- connection.add_limit_offset!(q, options)
194
+
195
+ limit = ETL::Engine.limit
196
+ offset = ETL::Engine.offset
197
+ if limit || offset
198
+ raise NoLimitSpecifiedError, "Specifying offset without limit is not allowed" if offset and limit.nil?
199
+ q << " LIMIT #{limit}"
200
+ q << " OFFSET #{offset}" if offset
198
201
  end
199
202
 
200
203
  q = q.gsub(/\n/,' ')
@@ -205,7 +208,7 @@ module ETL #:nodoc:
205
208
  def query_rows
206
209
  return @query_rows if @query_rows
207
210
  if (configuration[:mysqlstream] == true)
208
- MySqlStreamer.new(query,@target)
211
+ MySqlStreamer.new(query,@target,connection)
209
212
  else
210
213
  connection.select_all(query)
211
214
  end
@@ -1,31 +1,73 @@
1
1
  require 'open3'
2
2
 
3
+ # Internal: The MySQL streamer is a helper with works with the database_source
4
+ # in order to allow you to use the --quick option (which stops MySQL)
5
+ # from building a full result set, also we don't build a full resultset
6
+ # in Ruby - instead we yield a row at a time
7
+ #
3
8
  class MySqlStreamer
9
+ # Internal: Creates a MySQL Streamer
10
+ #
11
+ # query - the SQL query
12
+ # target - the name of the ETL configuration (ie. development/production)
13
+ # connection - the ActiveRecord connection
14
+ #
15
+ # Examples
16
+ #
17
+ # MySqlStreamer.new("select * from bob", "development", my_connection)
18
+ #
19
+ def initialize(query, target, connection)
20
+ # Lets just be safe and also make sure there aren't new lines
21
+ # in the SQL - its bound to cause trouble
22
+ @query = query.split.join(' ')
23
+ @name = target
24
+ @first_row = connection.select_all("#{query} LIMIT 1")
25
+ end
4
26
 
5
- def initialize(query, target)
6
- @query = query
7
- @name = target
8
- end
27
+ # We implement some bits of a hash so that database_source
28
+ # can use them
29
+ def any?
30
+ @first_row.any?
31
+ end
9
32
 
10
- def each
11
- puts "Using the Streaming MySQL from the command line"
12
- keys = nil
13
- connection_configuration = ETL::Base.configurations[@name.to_s]
14
- mysql_command = """mysql --quick -h #{connection_configuration["host"]} -u #{connection_configuration["username"]} -e \"#{@query.gsub("\n","")}\" -D #{connection_configuration["database"]} --password=#{connection_configuration["password"]} -B"""
15
- Open3.popen3(mysql_command) do |stdin, out, err, external|
16
- until (line = out.gets).nil? do
17
- line = line.gsub("\n","")
18
- if keys.nil?
19
- keys = line.split("\t")
20
- else
21
- hash = Hash[keys.zip(line.split("\t"))]
22
- yield hash
23
- end
24
- end
25
- error = err.gets
26
- if (!error.nil? && error.strip.length > 0)
27
- throw error
28
- end
29
- end
30
- end
33
+ def first
34
+ @first_row.first
35
+ end
36
+
37
+ def mandatory_option!(hash, key)
38
+ value = hash[key]
39
+ raise "Missing key #{key} in connection configuration #{@name}" if value.blank?
40
+ value
41
+ end
42
+
43
+ def each
44
+ keys = nil
45
+
46
+ config = ETL::Base.configurations[@name.to_s]
47
+ host = mandatory_option!(config, 'host')
48
+ username = mandatory_option!(config, 'username')
49
+ database = mandatory_option!(config, 'database')
50
+ password = config['password'] # this one can omitted in some cases
51
+
52
+ mysql_command = """mysql --quick -h #{host} -u #{username} -e \"#{@query.gsub("\n","")}\" -D #{database} --password=#{password} -B"""
53
+ Open3.popen3(mysql_command) do |stdin, out, err, external|
54
+ until (line = out.gets).nil? do
55
+ line = line.gsub("\n","")
56
+ if keys.nil?
57
+ keys = line.split("\t")
58
+ else
59
+ hash = Hash[keys.zip(line.split("\t"))]
60
+ # map out NULL to nil
61
+ hash.each do |k, v|
62
+ hash[k] = nil if v == 'NULL'
63
+ end
64
+ yield hash
65
+ end
66
+ end
67
+ error = err.gets
68
+ if (!error.nil? && error.strip.length > 0)
69
+ throw error
70
+ end
71
+ end
72
+ end
31
73
  end
@@ -49,8 +49,8 @@ module ETL #:nodoc:
49
49
  # * ETL::Control::Control instance
50
50
  # * ETL::Batch::Batch instance
51
51
  #
52
- # The process command will accept either a .ctl Control file or a .ebf
53
- # ETL Batch File.
52
+ # The process command will accept either a .ctl or .ctl.rb for a Control file or a .ebf
53
+ # or .ebf.rb for an ETL Batch File.
54
54
  def process(file)
55
55
  new().process(file)
56
56
  end
@@ -235,6 +235,14 @@ module ETL #:nodoc:
235
235
  def errors
236
236
  @errors ||= []
237
237
  end
238
+
239
+ # First attempt at centralizing error notifications
240
+ def track_error(control, msg)
241
+ errors << msg
242
+ control.error_handlers.each do |handler|
243
+ handler.call(msg)
244
+ end
245
+ end
238
246
 
239
247
  # Get a Hash of benchmark values where each value represents the total
240
248
  # amount of time in seconds spent processing in that portion of the ETL
@@ -264,8 +272,8 @@ module ETL #:nodoc:
264
272
  process(File.new(file))
265
273
  when File
266
274
  case file.path
267
- when /.ctl$/; process_control(file)
268
- when /.ebf$/; process_batch(file)
275
+ when /\.ctl(\.rb)?$/; process_control(file)
276
+ when /\.ebf(\.rb)?$/; process_batch(file)
269
277
  else raise RuntimeError, "Unsupported file type - #{file.path}"
270
278
  end
271
279
  when ETL::Control::Control
@@ -352,7 +360,8 @@ module ETL #:nodoc:
352
360
  end
353
361
  rescue => e
354
362
  msg = "Error processing rows after read from #{Engine.current_source} on line #{Engine.current_source_row}: #{e}"
355
- errors << msg
363
+ # TODO - track more information: row if possible, full exception...
364
+ track_error(control, msg)
356
365
  Engine.logger.error(msg)
357
366
  e.backtrace.each { |line| Engine.logger.error(line) }
358
367
  exceeded_error_threshold?(control) ? break : next
@@ -374,10 +383,10 @@ module ETL #:nodoc:
374
383
  end
375
384
  rescue ResolverError => e
376
385
  Engine.logger.error(e.message)
377
- errors << e.message
386
+ track_error(control, e.message)
378
387
  rescue => e
379
388
  msg = "Error transforming from #{Engine.current_source} on line #{Engine.current_source_row}: #{e}"
380
- errors << msg
389
+ track_error(control, msg)
381
390
  Engine.logger.error(msg)
382
391
  e.backtrace.each { |line| Engine.logger.error(line) }
383
392
  ensure
@@ -403,7 +412,7 @@ module ETL #:nodoc:
403
412
  end
404
413
  rescue => e
405
414
  msg = "Error processing rows before write from #{Engine.current_source} on line #{Engine.current_source_row}: #{e}"
406
- errors << msg
415
+ track_error(control, msg)
407
416
  Engine.logger.error(msg)
408
417
  e.backtrace.each { |line| Engine.logger.error(line) }
409
418
  exceeded_error_threshold?(control) ? break : next
@@ -423,7 +432,7 @@ module ETL #:nodoc:
423
432
  end
424
433
  rescue => e
425
434
  msg = "Error writing to #{Engine.current_destination}: #{e}"
426
- errors << msg
435
+ track_error(control, msg)
427
436
  Engine.logger.error msg
428
437
  e.backtrace.each { |line| Engine.logger.error(line) }
429
438
  exceeded_error_threshold?(control) ? break : next
@@ -5,6 +5,7 @@ module ETL #:nodoc:
5
5
  belongs_to :batch
6
6
  has_many :batches
7
7
  has_many :jobs
8
+ attr_accessible :batch_file, :status, :completed_at
8
9
  end
9
10
  end
10
11
  end
@@ -3,6 +3,7 @@ module ETL #:nodoc:
3
3
  # Persistent class representing an ETL job
4
4
  class Job < Base
5
5
  belongs_to :batch
6
+ attr_accessible :control_file, :status, :batch_id
6
7
  end
7
8
  end
8
- end
9
+ end
@@ -1,3 +1,3 @@
1
1
  module ETL#:nodoc:
2
- VERSION = "1.0.0.rc1"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -3,6 +3,7 @@
3
3
  # a bit hackish - tests would require a refactoring instead
4
4
 
5
5
  mysql2: &mysql2
6
+ host: localhost
6
7
  adapter: mysql2
7
8
  database: activewarehouse_etl_test
8
9
  username: root
@@ -1,3 +1,3 @@
1
1
  require File.dirname(__FILE__) + '/common'
2
2
 
3
- declare_gems '3.2.1'
3
+ declare_gems '3.2.3'
@@ -2,7 +2,7 @@ def declare_gems(activerecord_version)
2
2
  source :rubygems
3
3
 
4
4
  gem 'activerecord', activerecord_version
5
- gem 'adapter_extensions', :git => 'https://github.com/activewarehouse/adapter_extensions.git', :branch => 'rails-3'
5
+ gem 'adapter_extensions', :git => 'https://github.com/activewarehouse/adapter_extensions.git'
6
6
 
7
7
  if activerecord_version < '3.1'
8
8
  gem 'mysql2', '< 0.3'
@@ -33,6 +33,19 @@ CTL
33
33
 
34
34
  assert_equal 1, engine.errors.size
35
35
  end
36
+
37
+ should 'call error callbacks' do
38
+ engine = ETL::Engine.new
39
+
40
+ $our_errors = []
41
+ engine.process ETL::Control::Control.parse_text <<CTL
42
+ source :in, { :type => :enumerable, :enumerable => (1..100) }
43
+ on_error { |error| $our_errors << error }
44
+ after_read { |row| raise "Failure" }
45
+ CTL
46
+ assert_equal 100, $our_errors.size
47
+ assert_match /on line 100: Failure$/, $our_errors.last
48
+ end
36
49
 
37
50
  end
38
51
 
@@ -1,5 +1,14 @@
1
1
  require File.dirname(__FILE__) + '/test_helper'
2
2
 
3
+ # TODO - use FactoryGirl or similar
4
+ def build_source(options = {})
5
+ ETL::Control::DatabaseSource.new(nil, {
6
+ :target => 'operational_database',
7
+ :table => 'people',
8
+ :mysqlstream => true
9
+ }.merge(options), nil)
10
+ end
11
+
3
12
  class SourceTest < Test::Unit::TestCase
4
13
 
5
14
  context "source" do
@@ -84,6 +93,8 @@ class SourceTest < Test::Unit::TestCase
84
93
 
85
94
  context "a database source" do
86
95
  setup do
96
+ @offset = 2
97
+ @limit = 5
87
98
  control = ETL::Control::Control.parse(File.dirname(__FILE__) + '/delimited.ctl')
88
99
  configuration = {
89
100
  :database => 'etl_unittest',
@@ -97,6 +108,44 @@ class SourceTest < Test::Unit::TestCase
97
108
  ]
98
109
  @source = ETL::Control::DatabaseSource.new(control, configuration, definition)
99
110
  end
111
+
112
+ context "with a specified LIMIT `n`" do
113
+ setup do
114
+ ETL::Engine.limit = @limit
115
+ 10.times { |i| Person.create!( :first_name => 'Bob',
116
+ :last_name => 'Smith',
117
+ :ssn => i ) }
118
+ end
119
+
120
+ should "only return N rows" do
121
+ size = build_source(:store_locally => true, :mysqlstream => false).to_a.size
122
+ assert_equal 5, size
123
+ end
124
+
125
+ teardown do
126
+ Person.delete_all
127
+ ETL::Engine.limit = nil
128
+ end
129
+ end
130
+
131
+ context "with a specified OFFSET `offset`" do
132
+ setup do
133
+ ETL::Engine.limit = @limit
134
+ ETL::Engine.offset = @offset
135
+ end
136
+
137
+ should "raise an exception without LIMIT specified" do
138
+ ETL::Engine.limit = nil
139
+ assert_raise (NoLimitSpecifiedError) { build_source(:store_locally => true, :mysqlstream => false).to_a.size }
140
+ end
141
+
142
+ teardown do
143
+ Person.delete_all
144
+ ETL::Engine.limit = nil
145
+ ETL::Engine.offset = nil
146
+ end
147
+ end
148
+
100
149
  should "set the local file for extraction storage" do
101
150
  assert_match %r{source_data/localhost/activewarehouse_etl_test/people/\d+.csv}, @source.local_file.to_s
102
151
  end
@@ -108,6 +157,36 @@ class SourceTest < Test::Unit::TestCase
108
157
  rows = @source.collect { |row| row }
109
158
  assert_equal 1, rows.length
110
159
  end
160
+ if current_adapter =~ /mysql/
161
+ context 'with mysqlstream enabled' do
162
+
163
+ setup do
164
+ Person.delete_all
165
+ Person.create!(:first_name => 'Bob', :last_name => 'Smith', :ssn => '123456789')
166
+ Person.create!(:first_name => 'John', :last_name => 'Barry', :ssn => '123456790')
167
+ end
168
+
169
+ should 'support store_locally' do
170
+ assert_equal 2, build_source(:store_locally => true).to_a.size
171
+ end
172
+
173
+ context 'with a NULL value' do
174
+
175
+ should 'return nil in row attribute' do
176
+ Person.create!(:first_name => nil)
177
+ assert_equal nil, build_source.to_a.last[:first_name]
178
+ end
179
+
180
+ # does not work yet - we probably need a switch on --quick for this
181
+ should_eventually 'return NULL for string containing NULL' do
182
+ Person.create!(:first_name => 'NULL', :last_name => 'NULL2')
183
+ assert_equal 'NULL', build_source.to_a.last[:first_name]
184
+ end
185
+
186
+ end
187
+
188
+ end
189
+ end
111
190
  end
112
191
 
113
192
  context "a file source with an xml parser" do
@@ -30,3 +30,7 @@ puts "ActiveRecord::VERSION = #{ActiveRecord::VERSION::STRING}"
30
30
 
31
31
  class Person < ActiveRecord::Base
32
32
  end
33
+
34
+ def current_adapter
35
+ ENV['DB']
36
+ end
metadata CHANGED
@@ -1,8 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activewarehouse-etl
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.rc1
5
- prerelease: 6
4
+ version: 1.0.0
5
+ prerelease:
6
6
  platform: ruby
7
7
  authors:
8
8
  - Anthony Eden
@@ -10,11 +10,11 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-03-03 00:00:00.000000000 Z
13
+ date: 2012-06-20 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rake
17
- requirement: &70307704843740 !ruby/object:Gem::Requirement
17
+ requirement: &70217989126480 !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
20
  - - ! '>='
@@ -22,10 +22,10 @@ dependencies:
22
22
  version: 0.8.3
23
23
  type: :runtime
24
24
  prerelease: false
25
- version_requirements: *70307704843740
25
+ version_requirements: *70217989126480
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: activesupport
28
- requirement: &70307704843000 !ruby/object:Gem::Requirement
28
+ requirement: &70217989126020 !ruby/object:Gem::Requirement
29
29
  none: false
30
30
  requirements:
31
31
  - - ! '>='
@@ -33,10 +33,10 @@ dependencies:
33
33
  version: 3.0.0
34
34
  type: :runtime
35
35
  prerelease: false
36
- version_requirements: *70307704843000
36
+ version_requirements: *70217989126020
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: activerecord
39
- requirement: &70307704842100 !ruby/object:Gem::Requirement
39
+ requirement: &70217989125560 !ruby/object:Gem::Requirement
40
40
  none: false
41
41
  requirements:
42
42
  - - ! '>='
@@ -44,10 +44,10 @@ dependencies:
44
44
  version: 3.0.0
45
45
  type: :runtime
46
46
  prerelease: false
47
- version_requirements: *70307704842100
47
+ version_requirements: *70217989125560
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: fastercsv
50
- requirement: &70307704841200 !ruby/object:Gem::Requirement
50
+ requirement: &70217984932020 !ruby/object:Gem::Requirement
51
51
  none: false
52
52
  requirements:
53
53
  - - ! '>='
@@ -55,10 +55,10 @@ dependencies:
55
55
  version: 1.2.0
56
56
  type: :runtime
57
57
  prerelease: false
58
- version_requirements: *70307704841200
58
+ version_requirements: *70217984932020
59
59
  - !ruby/object:Gem::Dependency
60
60
  name: adapter_extensions
61
- requirement: &70307704840560 !ruby/object:Gem::Requirement
61
+ requirement: &70217984927180 !ruby/object:Gem::Requirement
62
62
  none: false
63
63
  requirements:
64
64
  - - ! '>='
@@ -66,10 +66,10 @@ dependencies:
66
66
  version: 0.9.5.rc1
67
67
  type: :runtime
68
68
  prerelease: false
69
- version_requirements: *70307704840560
69
+ version_requirements: *70217984927180
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: shoulda
72
- requirement: &70307704839660 !ruby/object:Gem::Requirement
72
+ requirement: &70217984925860 !ruby/object:Gem::Requirement
73
73
  none: false
74
74
  requirements:
75
75
  - - ~>
@@ -77,10 +77,10 @@ dependencies:
77
77
  version: 2.11.3
78
78
  type: :development
79
79
  prerelease: false
80
- version_requirements: *70307704839660
80
+ version_requirements: *70217984925860
81
81
  - !ruby/object:Gem::Dependency
82
82
  name: flexmock
83
- requirement: &70307704839120 !ruby/object:Gem::Requirement
83
+ requirement: &70217989152940 !ruby/object:Gem::Requirement
84
84
  none: false
85
85
  requirements:
86
86
  - - ~>
@@ -88,10 +88,10 @@ dependencies:
88
88
  version: 0.9.0
89
89
  type: :development
90
90
  prerelease: false
91
- version_requirements: *70307704839120
91
+ version_requirements: *70217989152940
92
92
  - !ruby/object:Gem::Dependency
93
93
  name: cartesian
94
- requirement: &70307704838600 !ruby/object:Gem::Requirement
94
+ requirement: &70217989152560 !ruby/object:Gem::Requirement
95
95
  none: false
96
96
  requirements:
97
97
  - - ! '>='
@@ -99,7 +99,29 @@ dependencies:
99
99
  version: '0'
100
100
  type: :development
101
101
  prerelease: false
102
- version_requirements: *70307704838600
102
+ version_requirements: *70217989152560
103
+ - !ruby/object:Gem::Dependency
104
+ name: guard
105
+ requirement: &70217989152100 !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ type: :development
112
+ prerelease: false
113
+ version_requirements: *70217989152100
114
+ - !ruby/object:Gem::Dependency
115
+ name: guard-shell
116
+ requirement: &70217989151680 !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ! '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ type: :development
123
+ prerelease: false
124
+ version_requirements: *70217989151680
103
125
  description: ActiveWarehouse ETL is a pure Ruby Extract-Transform-Load application
104
126
  for loading data into a database.
105
127
  email:
@@ -115,6 +137,7 @@ files:
115
137
  - 0.9-UPGRADE
116
138
  - CHANGELOG
117
139
  - Gemfile
140
+ - Guardfile
118
141
  - HOW_TO_RELEASE
119
142
  - LICENSE
120
143
  - README.textile