activewarehouse-etl 1.0.0.rc1 → 1.0.0

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/.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