abstract_importer 1.1.0 → 1.2.0.rc1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 390699648f23aa027bf392f0f9095bf4b6801e3b
4
- data.tar.gz: 5f1d0ccb8c4db21649913fa3914b7f6a4f0ba22c
3
+ metadata.gz: df7b7a0811459f30502cf09d03741312919b9f54
4
+ data.tar.gz: 116840924377d0562121169b715b9fd64f49fde1
5
5
  SHA512:
6
- metadata.gz: 1081c7623c65fcbd0748385d6e30f139dcf0d33703ace0c172e9b3e2d9a07aac3cabff0de5f600dc40dc0de7d28af5de2d0865960252888bb5b101711c8ed252
7
- data.tar.gz: b59ab4abc7f8c9634e00c24840a01dfe0974f5b7f57e8f34b57129ad10e8d7f0c226972c3323b67107b3aeec6741b2f9e7aaf072ccc8515dbae308c59ca8072b
6
+ metadata.gz: a9c6b45e74533ba5a1ef07432d677834057649b66656c07c61594ea34e91bb449e5346167fb69cad7d7c464a1c48899e420f8f73a051a22b731afeecfab1db49
7
+ data.tar.gz: b64e4b346bbc2f566c5686b95edde07f3f49f86be9a971916ac870a3d583038ef31da07cad2ed4e7d58f9f976c6f4de92343dfd93c7449841523cafd1488176d
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # AbstractImporter
2
2
 
3
3
  [![Build Status](https://travis-ci.org/concordia-publishing-house/abstract_importer.png?branch=master)](https://travis-ci.org/concordia-publishing-house/abstract_importer)
4
-
5
4
  [![Code Climate](https://codeclimate.com/github/concordia-publishing-house/abstract_importer.png)](https://codeclimate.com/github/concordia-publishing-house/abstract_importer)
6
5
 
7
6
  AbstractImporter provides services for importing complex data from an arbitrary data source. It:
@@ -54,7 +53,7 @@ end
54
53
  `MyImporter`'s initializer takes two arguments: `parent` and `data_source`:
55
54
 
56
55
  * `parent` is any object that will respond to the names of your collections with an `ActiveRecord::Relation`.
57
- * `data_source` is any object that will respond to the names of your collections by yielding a hash of attributes once for every record you should import.
56
+ * `data_source` is any object that will respond to the names of your collections with an `Enumerator`.
58
57
 
59
58
  Here are reasonable classes for `parent` and `data_source`:
60
59
 
@@ -70,15 +69,19 @@ end
70
69
  # data source
71
70
  class Database
72
71
  def students
73
- yield id: 457, name: "Ron"
74
- yield id: 458, name: "Ginny"
75
- yield id: 459, name: "Fred"
76
- yield id: 460, name: "George"
72
+ Enumerator.new do |e|
73
+ e.yield id: 457, name: "Ron"
74
+ e.yield id: 458, name: "Ginny"
75
+ e.yield id: 459, name: "Fred"
76
+ e.yield id: 460, name: "George"
77
+ end
77
78
  end
78
79
 
79
80
  def parents
80
- yield id: 88, name: "Arthur"
81
- yield id: 89, name: "Molly"
81
+ Enumerator.new do |e|
82
+ e.yield id: 88, name: "Arthur"
83
+ e.yield id: 89, name: "Molly"
84
+ end
82
85
  end
83
86
  end
84
87
  ```
@@ -118,7 +121,7 @@ class MyImporter < AbstractImporter::Base
118
121
  import.students do |options|
119
122
  options.finder :find_student
120
123
  options.before_build { |attrs| attrs.merge(name: attrs[:name].capitalize) }
121
- options.on_complete :students_completed
124
+ options.after_all :students_completed
122
125
  end
123
126
  import.parents
124
127
  end
@@ -156,9 +159,13 @@ The complete list of callbacks is below.
156
159
 
157
160
  `after_create` is called with the original hash of attributes and the newly-saved record right after it is successfully saved.
158
161
 
159
- ##### on_complete
162
+ ##### before_all
163
+
164
+ `before_all` is called just before the records in a collection are been processed.
165
+
166
+ ##### after_all
160
167
 
161
- `on_complete` is called when all of the records in a collection have been processed.
168
+ `after_all` is called when all of the records in a collection have been processed.
162
169
 
163
170
 
164
171
 
@@ -15,7 +15,11 @@ module AbstractImporter
15
15
  yield @import_plan = ImportPlan.new
16
16
  end
17
17
 
18
- attr_reader :import_plan
18
+ def depends_on(*dependencies)
19
+ @dependencies = dependencies
20
+ end
21
+
22
+ attr_reader :import_plan, :dependencies
19
23
  end
20
24
 
21
25
 
@@ -31,11 +35,16 @@ module AbstractImporter
31
35
  @id_map = IdMap.new
32
36
  @results = {}
33
37
  @import_plan = self.class.import_plan.to_h
38
+ @atomic = options.fetch(:atomic, false)
34
39
  @collections = []
35
40
  end
36
41
 
37
42
  attr_reader :source, :parent, :reporter, :id_map, :results
38
43
 
44
+ def atomic?
45
+ @atomic
46
+ end
47
+
39
48
  def dry_run?
40
49
  @dry_run
41
50
  end
@@ -53,7 +62,9 @@ module AbstractImporter
53
62
  reporter.finish_setup(ms)
54
63
 
55
64
  ms = Benchmark.ms do
56
- collections.each &method(:import_collection)
65
+ with_transaction do
66
+ collections.each &method(:import_collection)
67
+ end
57
68
  end
58
69
 
59
70
  teardown
@@ -142,8 +153,19 @@ module AbstractImporter
142
153
  end
143
154
  end
144
155
 
156
+ def dependencies
157
+ @dependencies ||= Array(self.class.dependencies).map do |name|
158
+ reflection = parent.class.reflect_on_association(name)
159
+ model = reflection.klass
160
+ table_name = model.table_name
161
+ scope = parent.public_send(name)
162
+
163
+ Collection.new(name, model, table_name, scope, nil)
164
+ end
165
+ end
166
+
145
167
  def prepopulate_id_map!
146
- collections.each do |collection|
168
+ (collections + dependencies).each do |collection|
147
169
  query = collection.scope.where("#{collection.table_name}.legacy_id IS NOT NULL")
148
170
  map = values_of(query, :id, :legacy_id) \
149
171
  .each_with_object({}) { |(id, legacy_id), map| map[legacy_id] = id }
@@ -167,5 +189,15 @@ module AbstractImporter
167
189
  reporter.count_notice "#{plural}.#{foreign_key} will be nil: a #{depends_on.to_s.singularize} with the legacy id #{legacy_id} was not mapped."
168
190
  end
169
191
 
192
+
193
+
194
+ def with_transaction(&block)
195
+ if atomic?
196
+ ActiveRecord::Base.transaction(requires_new: true, &block)
197
+ else
198
+ block.call
199
+ end
200
+ end
201
+
170
202
  end
171
203
  end
@@ -30,11 +30,12 @@ module AbstractImporter
30
30
  reporter.start_collection(self)
31
31
  prepare!
32
32
 
33
+ invoke_callback(:before_all)
33
34
  summary.ms = Benchmark.ms do
34
35
  each_new_record &method(:process_record)
35
36
  end
37
+ invoke_callback(:after_all)
36
38
 
37
- invoke_callback(:on_complete)
38
39
  reporter.finish_collection(self, summary)
39
40
  summary
40
41
  end
@@ -54,43 +55,56 @@ module AbstractImporter
54
55
  # has foreign keys that refer to another
55
56
  next unless association.macro == :belongs_to
56
57
 
57
- # We don't at this time support polymorphic associations
58
- # which would require extending id_map to take the foreign
59
- # type fields into account.
60
- #
61
- # Rails can't return `association.table_name` so easily
62
- # because `table_name` comes from `klass` and `klass`
63
- # isn't predetermined.
64
- next if association.options[:polymorphic]
65
-
66
- depends_on = association.table_name.to_sym
67
- foreign_key = association.foreign_key.to_sym
68
-
69
58
  # We support skipping some mappings entirely. I believe
70
59
  # this is largely to cut down on verbosity in the log
71
60
  # files and should be refactored to another place in time.
61
+ foreign_key = association.foreign_key.to_sym
72
62
  next unless remap_foreign_key?(name, foreign_key)
73
63
 
74
- mappings << Proc.new do |hash|
75
- if hash.key?(foreign_key)
76
- hash[foreign_key] = map_foreign_key(hash[foreign_key], name, foreign_key, depends_on)
77
- else
78
- reporter.count_notice "#{name}.#{foreign_key} will not be mapped because it is not used"
79
- end
64
+ if association.options[:polymorphic]
65
+ mappings << prepare_polymorphic_mapping!(association)
66
+ else
67
+ mappings << prepare_mapping!(association)
80
68
  end
81
69
  end
82
70
  mappings
83
71
  end
84
72
 
73
+ def prepare_mapping!(association)
74
+ depends_on = association.table_name.to_sym
75
+ foreign_key = association.foreign_key.to_sym
76
+
77
+ Proc.new do |attrs|
78
+ if attrs.key?(foreign_key)
79
+ attrs[foreign_key] = map_foreign_key(attrs[foreign_key], name, foreign_key, depends_on)
80
+ else
81
+ reporter.count_notice "#{name}.#{foreign_key} will not be mapped because it is not used"
82
+ end
83
+ end
84
+ end
85
+
86
+ def prepare_polymorphic_mapping!(association)
87
+ foreign_key = association.foreign_key.to_sym
88
+ foreign_type = association.foreign_key.gsub(/_id$/, "_type").to_sym
89
+
90
+ Proc.new do |attrs|
91
+ if attrs.key?(foreign_key) && attrs.key?(foreign_type)
92
+ foreign_model = attrs[foreign_type]
93
+ depends_on = foreign_model.tableize.to_sym if foreign_model
94
+ attrs[foreign_key] = depends_on && map_foreign_key(attrs[foreign_key], name, foreign_key, depends_on)
95
+ else
96
+ reporter.count_notice "#{name}.#{foreign_key} will not be mapped because it is not used"
97
+ end
98
+ end
99
+ end
100
+
85
101
 
86
102
 
87
103
 
88
104
 
89
105
  def each_new_record
90
- source.public_send(name) do |hash_or_hashes|
91
- Array.wrap(hash_or_hashes).each do |hash|
92
- yield hash.dup
93
- end
106
+ source.public_send(name).each do |attrs|
107
+ yield attrs.dup
94
108
  end
95
109
  end
96
110
 
@@ -114,6 +128,8 @@ module AbstractImporter
114
128
  else
115
129
  summary.invalid += 1
116
130
  end
131
+ rescue ::AbstractImporter::Skip
132
+ summary.skipped += 1
117
133
  end
118
134
 
119
135
 
@@ -154,7 +170,7 @@ module AbstractImporter
154
170
  # rescue_callback has one shot to fix things
155
171
  invoke_callback(:rescue, record) unless record.valid?
156
172
 
157
- if record.save
173
+ if record.valid? && record.save
158
174
  invoke_callback(:after_create, hash, record)
159
175
  id_map << record
160
176
 
@@ -162,7 +178,7 @@ module AbstractImporter
162
178
  true
163
179
  else
164
180
 
165
- reporter.record_failed(record)
181
+ reporter.record_failed(record, hash)
166
182
  false
167
183
  end
168
184
  end
@@ -186,4 +202,7 @@ module AbstractImporter
186
202
  end
187
203
 
188
204
  end
205
+
206
+ class Skip < StandardError; end
207
+
189
208
  end
@@ -5,7 +5,8 @@ module AbstractImporter
5
5
  :before_build_callback,
6
6
  :before_create_callback,
7
7
  :after_create_callback,
8
- :on_complete_callback
8
+ :before_all_callback,
9
+ :after_all_callback
9
10
 
10
11
  def finder(sym=nil, &block)
11
12
  @finder_callback = sym || block
@@ -27,8 +28,12 @@ module AbstractImporter
27
28
  @rescue_callback = sym || block
28
29
  end
29
30
 
30
- def on_complete(sym=nil, &block)
31
- @on_complete_callback = sym || block
31
+ def before_all(sym=nil, &block)
32
+ @before_all_callback = sym || block
33
+ end
34
+
35
+ def after_all(sym=nil, &block)
36
+ @after_all_callback = sym || block
32
37
  end
33
38
 
34
39
  end
@@ -54,7 +54,7 @@ module AbstractImporter
54
54
  io.print "." unless production?
55
55
  end
56
56
 
57
- def record_failed(record)
57
+ def record_failed(record, hash)
58
58
  io.print "×" unless production?
59
59
 
60
60
  error_messages = invalid_params[record.class.name] ||= {}
@@ -111,8 +111,9 @@ module AbstractImporter
111
111
  stat "#{summary.already_imported} #{plural} were imported previously"
112
112
  stat "#{summary.redundant} #{plural} would create duplicates and will not be imported"
113
113
  stat "#{summary.invalid} #{plural} were invalid"
114
+ stat "#{summary.skipped} #{plural} were skipped"
114
115
  stat "#{summary.created} #{plural} were imported"
115
- stat "#{distance_of_time(summary.ms)} elapsed (#{(summary.ms / summary.total).to_i}ms each)"
116
+ stat "#{distance_of_time(summary.ms)} elapsed (#{summary.average_ms.to_i}ms each)"
116
117
  else
117
118
  stat "#{distance_of_time(summary.ms)} elapsed"
118
119
  end
@@ -1,8 +1,13 @@
1
1
  module AbstractImporter
2
- class Summary < Struct.new(:total, :redundant, :created, :already_imported, :invalid, :ms)
2
+ class Summary < Struct.new(:total, :redundant, :created, :already_imported, :invalid, :ms, :skipped)
3
3
 
4
4
  def initialize
5
- super(0,0,0,0,0,0)
5
+ super(0,0,0,0,0,0,0)
6
+ end
7
+
8
+ def average_ms
9
+ return nil if total == 0
10
+ ms / total
6
11
  end
7
12
 
8
13
  end
@@ -1,3 +1,3 @@
1
1
  module AbstractImporter
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0.rc1"
3
3
  end
@@ -18,6 +18,20 @@ class CallbackTest < ActiveSupport::TestCase
18
18
  import!
19
19
  assert_equal ["Harry", "Ron", "Hermione"], account.students.pluck(:name)
20
20
  end
21
+
22
+ should "allow you to skip certain records" do
23
+ plan do |import|
24
+ import.students do |options|
25
+ options.before_build do |attrs|
26
+ raise AbstractImporter::Skip if attrs[:name] == "Harry Potter"
27
+ end
28
+ end
29
+ end
30
+
31
+ import!
32
+ assert_equal ["Ron Weasley", "Hermione Granger"], account.students.pluck(:name)
33
+ assert_equal 1, results[:students].skipped
34
+ end
21
35
  end
22
36
 
23
37
 
@@ -75,11 +89,28 @@ class CallbackTest < ActiveSupport::TestCase
75
89
 
76
90
 
77
91
 
78
- context "on_complete" do
92
+ context "before_all" do
93
+ setup do
94
+ plan do |import|
95
+ import.students do |options|
96
+ options.before_all :callback
97
+ end
98
+ end
99
+ end
100
+
101
+ should "should be invoked before the collection has been imported" do
102
+ mock(importer).callback.once
103
+ import!
104
+ end
105
+ end
106
+
107
+
108
+
109
+ context "after_all" do
79
110
  setup do
80
111
  plan do |import|
81
112
  import.students do |options|
82
- options.on_complete :callback
113
+ options.after_all :callback
83
114
  end
84
115
  end
85
116
  end
@@ -45,7 +45,44 @@ class ImporterTest < ActiveSupport::TestCase
45
45
  assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
46
46
  end
47
47
 
48
- should "preserve mappings event when a record was previously imported" do
48
+ should "preserve mappings even when a record was previously imported" do
49
+ harry = account.students.create!(name: "Harry Potter", legacy_id: 456)
50
+ import!
51
+ assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
52
+ end
53
+
54
+ context "when {atomic: true}" do
55
+ should "rollback the whole import if an part fails" do
56
+ mock(importer).atomic? { true }
57
+ mock.instance_of(Parent).save { raise "hell" }
58
+ import! rescue
59
+ assert_equal 0, account.parents.count, "No parents should have been imported with the exception"
60
+ assert_equal 0, account.students.count, "Expected students to have been rolled back"
61
+ end
62
+ end
63
+
64
+ context "when {atomic: false}" do
65
+ should "not rollback the whole import if an part fails" do
66
+ mock(importer).atomic? { false }
67
+ mock.instance_of(Parent).save { raise "hell" }
68
+ import! rescue
69
+ assert_equal 0, account.parents.count, "No parents should have been imported with the exception"
70
+ assert_equal 3, account.students.count, "Expected students not to have been rolled back"
71
+ end
72
+ end
73
+ end
74
+
75
+
76
+
77
+ context "with a dependency" do
78
+ setup do
79
+ depends_on :students
80
+ plan do |import|
81
+ import.parents
82
+ end
83
+ end
84
+
85
+ should "preserve mappings when a dependency was imported by another importer" do
49
86
  harry = account.students.create!(name: "Harry Potter", legacy_id: 456)
50
87
  import!
51
88
  assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
@@ -103,4 +140,23 @@ class ImporterTest < ActiveSupport::TestCase
103
140
 
104
141
 
105
142
 
143
+ context "with polymorphic associations" do
144
+ setup do
145
+ plan do |import|
146
+ import.cats
147
+ import.owls
148
+ import.students
149
+ end
150
+ end
151
+
152
+ should "preserve mappings" do
153
+ import!
154
+ assert_equal 2, account.students.map(&:pet).compact.count, "Expected two students to still be linked to their pets upon import"
155
+ assert_kind_of Owl, account.students.find_by_name("Harry Potter").pet, "Expected Harry's pet to be an Owl"
156
+ assert_kind_of Cat, account.students.find_by_name("Hermione Granger").pet, "Expected Harry's pet to be a Cat"
157
+ end
158
+ end
159
+
160
+
161
+
106
162
  end
@@ -1,32 +1,56 @@
1
1
  class MockDataSource
2
2
 
3
+
3
4
  def students
4
- yield id: 456, name: "Harry Potter"
5
- yield id: 457, name: "Ron Weasley"
6
- yield id: 458, name: "Hermione Granger"
5
+ Enumerator.new do |e|
6
+ e.yield id: 456, name: "Harry Potter", pet_type: "Owl", pet_id: 901
7
+ e.yield id: 457, name: "Ron Weasley", pet_type: nil, pet_id: nil
8
+ e.yield id: 458, name: "Hermione Granger", pet_type: "Cat", pet_id: 901
9
+ end
7
10
  end
8
11
 
9
12
  def parents
10
- yield id: 88, name: "James Potter", student_id: 456
11
- yield id: 89, name: "Lily Potter", student_id: 456
13
+ Enumerator.new do |e|
14
+ e.yield id: 88, name: "James Potter", student_id: 456
15
+ e.yield id: 89, name: "Lily Potter", student_id: 456
16
+ end
12
17
  end
13
18
 
14
19
  def locations
15
- yield id: 5, slug: "godric's-hollow" # <-- invalid
16
- yield id: 6, slug: "azkaban"
20
+ Enumerator.new do |e|
21
+ e.yield id: 5, slug: "godric's-hollow" # <-- invalid
22
+ e.yield id: 6, slug: "azkaban"
23
+ end
17
24
  end
18
25
 
19
26
  def subjects
20
- yield id: 49, name: "Care of Magical Creatures", student_ids: [456]
21
- yield id: 50, name: "Advanced Potions", student_ids: [456, 457]
22
- yield id: 51, name: "History of Magic", student_ids: [457]
23
- yield id: 52, name: "Arithmancy", student_ids: [458]
24
- yield id: 53, name: "Study of Ancient Runes", student_ids: [458]
27
+ Enumerator.new do |e|
28
+ e.yield id: 49, name: "Care of Magical Creatures", student_ids: [456]
29
+ e.yield id: 50, name: "Advanced Potions", student_ids: [456, 457]
30
+ e.yield id: 51, name: "History of Magic", student_ids: [457]
31
+ e.yield id: 52, name: "Arithmancy", student_ids: [458]
32
+ e.yield id: 53, name: "Study of Ancient Runes", student_ids: [458]
33
+ end
25
34
  end
26
35
 
27
36
  def grades
28
- yield id: 500, subject_id: 50, student_id: 457, value: "Acceptable"
29
- yield id: 501, subject_id: 51, student_id: 457, value: "Troll"
37
+ Enumerator.new do |e|
38
+ e.yield id: 500, subject_id: 50, student_id: 457, value: "Acceptable"
39
+ e.yield id: 501, subject_id: 51, student_id: 457, value: "Troll"
40
+ end
41
+ end
42
+
43
+ def cats
44
+ Enumerator.new do |e|
45
+ e.yield id: 901, name: "Crookshanks"
46
+ end
30
47
  end
31
48
 
49
+ def owls
50
+ Enumerator.new do |e|
51
+ e.yield id: 901, name: "Hedwig"
52
+ end
53
+ end
54
+
55
+
32
56
  end
@@ -2,6 +2,7 @@ class Student < ActiveRecord::Base
2
2
  has_and_belongs_to_many :subjects
3
3
  has_many :grades
4
4
  has_many :parents
5
+ belongs_to :pet, polymorphic: true
5
6
 
6
7
  def report_card
7
8
  subjects.map do |subject|
@@ -34,4 +35,14 @@ class Account < ActiveRecord::Base
34
35
  has_many :subjects
35
36
  has_many :grades
36
37
  has_many :locations
38
+ has_many :cats
39
+ has_many :owls
40
+ end
41
+
42
+ class Cat < ActiveRecord::Base
43
+ has_one :student, as: :pet
44
+ end
45
+
46
+ class Owl < ActiveRecord::Base
47
+ has_one :student, as: :pet
37
48
  end
@@ -8,6 +8,8 @@ ActiveRecord::Schema.define(:version => 1) do
8
8
  t.integer "legacy_id"
9
9
  t.string "name"
10
10
  t.string "house"
11
+ t.string "pet_type"
12
+ t.integer "pet_id"
11
13
  end
12
14
 
13
15
  create_table "parents", :force => true do |t|
@@ -42,4 +44,16 @@ ActiveRecord::Schema.define(:version => 1) do
42
44
  t.string "value"
43
45
  end
44
46
 
47
+ create_table "owls", :force => true do |t|
48
+ t.integer "account_id"
49
+ t.integer "legacy_id"
50
+ t.string "name"
51
+ end
52
+
53
+ create_table "cats", :force => true do |t|
54
+ t.integer "account_id"
55
+ t.integer "legacy_id"
56
+ t.string "name"
57
+ end
58
+
45
59
  end
@@ -50,12 +50,16 @@ class ActiveSupport::TestCase
50
50
 
51
51
  protected
52
52
 
53
- attr_reader :account, :results
53
+ attr_reader :account, :results, :data_source
54
54
 
55
55
  def plan(&block)
56
56
  @klass.import(&block)
57
57
  end
58
58
 
59
+ def depends_on(*args)
60
+ @klass.depends_on(*args)
61
+ end
62
+
59
63
  def import!
60
64
  @results = importer.perform!
61
65
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: abstract_importer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Lail
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-10-19 00:00:00.000000000 Z
11
+ date: 2014-04-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -209,12 +209,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
209
209
  version: '0'
210
210
  required_rubygems_version: !ruby/object:Gem::Requirement
211
211
  requirements:
212
- - - '>='
212
+ - - '>'
213
213
  - !ruby/object:Gem::Version
214
- version: '0'
214
+ version: 1.3.1
215
215
  requirements: []
216
216
  rubyforge_project:
217
- rubygems_version: 2.0.3
217
+ rubygems_version: 2.2.1
218
218
  signing_key:
219
219
  specification_version: 4
220
220
  summary: Provides services for the mass-import of complex relational data