csvbuilder-importer 0.1.3 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/Gemfile.lock +89 -0
- data/README.md +207 -5
- data/gemfiles/rails_edge.gemfile +17 -0
- data/lib/csvbuilder/importer/internal/import/csv.rb +2 -2
- data/lib/csvbuilder/importer/public/import/file.rb +10 -2
- data/lib/csvbuilder/importer/version.rb +1 -1
- metadata +5 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b630db32ac5aff6725cbdb9f66414aed575f0b7e5529dd982af31f2002c0ca88
|
4
|
+
data.tar.gz: d474369caa5696069c0273ca414e27e7f4aeb281e20312682fd99ec96d5880cc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d188fde51bb5aafa126c10b0493f49ea085141d752cce03a4ed34c33ecbbc21712d3a06a3cb3232d2c56f9c40bd3b400224015ac42139e502844bdca497b1c06
|
7
|
+
data.tar.gz: c58f400d80291c30e4b9198381240b7a7ecfa4ddb0db4266b4fcd6eb0a744a0d7a7902a3f448ba8b73f8438a0c913c7195794750d2388aecee868fdccf8a3ba6
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [Released]
|
4
|
+
|
5
|
+
## [0.1.5] - 2023-07-26
|
6
|
+
|
7
|
+
- Add a way to abort at the importer level, handy to handle wrong headers check https://github.com/joel/csvbuilder-importer/pull/12
|
8
|
+
- Using Less Memory And Quicker Line Counter https://github.com/joel/csvbuilder-importer/pull/11
|
9
|
+
|
10
|
+
## [0.1.4] - 2023-04-21
|
11
|
+
|
12
|
+
- Potential Security Fix
|
13
|
+
|
14
|
+
https://github.com/joel/csvbuilder-importer/compare/v0.1.2...v0.1.4
|
15
|
+
|
3
16
|
## [0.1.0] - 2022-12-16
|
4
17
|
|
5
18
|
- Initial release
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
csvbuilder-importer (0.1.5)
|
5
|
+
activemodel (>= 5.2)
|
6
|
+
activesupport (>= 5.2)
|
7
|
+
csvbuilder-core
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activemodel (7.0.4.2)
|
13
|
+
activesupport (= 7.0.4.2)
|
14
|
+
activesupport (7.0.4.2)
|
15
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
16
|
+
i18n (>= 1.6, < 2)
|
17
|
+
minitest (>= 5.1)
|
18
|
+
tzinfo (~> 2.0)
|
19
|
+
ast (2.4.2)
|
20
|
+
concurrent-ruby (1.2.2)
|
21
|
+
csvbuilder-core (0.1.3)
|
22
|
+
activesupport (>= 5.2, < 8)
|
23
|
+
diff-lcs (1.5.0)
|
24
|
+
i18n (1.12.0)
|
25
|
+
concurrent-ruby (~> 1.0)
|
26
|
+
json (2.6.3)
|
27
|
+
minitest (5.17.0)
|
28
|
+
parallel (1.23.0)
|
29
|
+
parser (3.2.2.0)
|
30
|
+
ast (~> 2.4.1)
|
31
|
+
rainbow (3.1.1)
|
32
|
+
rake (13.0.6)
|
33
|
+
regexp_parser (2.8.0)
|
34
|
+
rexml (3.2.5)
|
35
|
+
rspec (3.12.0)
|
36
|
+
rspec-core (~> 3.12.0)
|
37
|
+
rspec-expectations (~> 3.12.0)
|
38
|
+
rspec-mocks (~> 3.12.0)
|
39
|
+
rspec-core (3.12.1)
|
40
|
+
rspec-support (~> 3.12.0)
|
41
|
+
rspec-expectations (3.12.2)
|
42
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
43
|
+
rspec-support (~> 3.12.0)
|
44
|
+
rspec-mocks (3.12.3)
|
45
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
46
|
+
rspec-support (~> 3.12.0)
|
47
|
+
rspec-support (3.12.0)
|
48
|
+
rubocop (1.50.2)
|
49
|
+
json (~> 2.3)
|
50
|
+
parallel (~> 1.10)
|
51
|
+
parser (>= 3.2.0.0)
|
52
|
+
rainbow (>= 2.2.2, < 4.0)
|
53
|
+
regexp_parser (>= 1.8, < 3.0)
|
54
|
+
rexml (>= 3.2.5, < 4.0)
|
55
|
+
rubocop-ast (>= 1.28.0, < 2.0)
|
56
|
+
ruby-progressbar (~> 1.7)
|
57
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
58
|
+
rubocop-ast (1.28.0)
|
59
|
+
parser (>= 3.2.1.0)
|
60
|
+
rubocop-capybara (2.18.0)
|
61
|
+
rubocop (~> 1.41)
|
62
|
+
rubocop-performance (1.17.1)
|
63
|
+
rubocop (>= 1.7.0, < 2.0)
|
64
|
+
rubocop-ast (>= 0.4.0)
|
65
|
+
rubocop-rake (0.6.0)
|
66
|
+
rubocop (~> 1.0)
|
67
|
+
rubocop-rspec (2.20.0)
|
68
|
+
rubocop (~> 1.33)
|
69
|
+
rubocop-capybara (~> 2.17)
|
70
|
+
ruby-progressbar (1.13.0)
|
71
|
+
tzinfo (2.0.6)
|
72
|
+
concurrent-ruby (~> 1.0)
|
73
|
+
unicode-display_width (2.4.2)
|
74
|
+
|
75
|
+
PLATFORMS
|
76
|
+
arm64-darwin-22
|
77
|
+
x86_64-linux
|
78
|
+
|
79
|
+
DEPENDENCIES
|
80
|
+
csvbuilder-importer!
|
81
|
+
rake
|
82
|
+
rspec
|
83
|
+
rubocop
|
84
|
+
rubocop-performance
|
85
|
+
rubocop-rake
|
86
|
+
rubocop-rspec
|
87
|
+
|
88
|
+
BUNDLED WITH
|
89
|
+
2.4.7
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# Csvbuilder::Importer
|
2
2
|
|
3
|
-
|
3
|
+
[Csvbuilder::Importer](https://github.com/joel/csvbuilder-importer) is part of the [csvbuilder-collection](https://github.com/joel/csvbuilder)
|
4
4
|
|
5
|
-
|
5
|
+
The importer contains the implementation for importing data from a CSV file.
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
@@ -14,9 +14,211 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
14
14
|
|
15
15
|
$ gem install csvbuilder-importer
|
16
16
|
|
17
|
-
|
17
|
+
# Usage
|
18
18
|
|
19
|
-
|
19
|
+
Importing data from a CSV is critical and requires two validation layers. First, you need to ensure data from the CSV are correct, and Second, you need to check that data respects the business logic before inserting it into the system.
|
20
|
+
|
21
|
+
To do that, `Csvbuilder::Import` use `ActiveModel::Validations` so you can write your validations in the `CsvImportModel`.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class UserCsvImportModel
|
25
|
+
include Csvbuilder::Model
|
26
|
+
include Csvbuilder::Import
|
27
|
+
|
28
|
+
column :first_name
|
29
|
+
column :last_name
|
30
|
+
|
31
|
+
validates :first_name, presence: true, length: { minimum: 2 }
|
32
|
+
|
33
|
+
def abort?
|
34
|
+
"#{first_name} #{last_name}" == "Bill Gates"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
The import takes the CSV file and the Import class.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
rows = Csvbuilder::Import::File.new(file.path, UserCsvImportModel).each
|
43
|
+
row_enumerator = rows.each # It's essential to go through the Enumerator to benefit from the callbacks. See References[^1]
|
44
|
+
row_model_instance = row_enumerator.next
|
45
|
+
```
|
46
|
+
|
47
|
+
`Csvbuilder::Import` implement two essential methods:
|
48
|
+
|
49
|
+
1. skip?
|
50
|
+
2. abort? # NOTE: abort can be trigger at the importer level too.
|
51
|
+
|
52
|
+
You have to provide your implementation of the method `abort?`. If the method `abort?` returns true, the iteration will stop.
|
53
|
+
|
54
|
+
By default, `skip?` return true if the `CsvImportClass` is invalid, but it is safe to override. This means the previous iteration will not return any invalid row.
|
55
|
+
|
56
|
+
# Integration
|
57
|
+
|
58
|
+
Let's say we want to do something useful and add users if those users are valid.
|
59
|
+
|
60
|
+
Let's extract the `CsvRowModel` for more clarity:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class UserCsvRowModel
|
64
|
+
include Csvbuilder::Model
|
65
|
+
|
66
|
+
column :first_name
|
67
|
+
column :last_name
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
class UserCsvImportModel < UserCsvRowModel
|
73
|
+
include Csvbuilder::Import
|
74
|
+
|
75
|
+
validates :first_name, presence: true, length: { minimum: 2 }
|
76
|
+
validates :last_name, presence: true, length: { minimum: 2 }
|
77
|
+
|
78
|
+
def user
|
79
|
+
User.new(first_name: first_name, last_name: last_name)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Skip if the row is not valid,
|
83
|
+
# the user is not valid or
|
84
|
+
# the user already exists
|
85
|
+
def skip?
|
86
|
+
super || !user.valid? || user.exists?
|
87
|
+
end
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
Now, we can safely import our users.
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
[
|
95
|
+
["First name", "Last name"],
|
96
|
+
["John" , "Doe" ],
|
97
|
+
]
|
98
|
+
|
99
|
+
Csvbuilder::Import::File.new(file.path, UserCsvImportModel).each do |row_model|
|
100
|
+
row_model.user.save
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
# Advance Integration
|
105
|
+
|
106
|
+
`Csvbuilder::Import::File` implement callbacks. It provides the following:
|
107
|
+
|
108
|
+
1. before_each_iteration
|
109
|
+
2. around_each_iteration
|
110
|
+
3. after_each_iteration
|
111
|
+
4. before_next
|
112
|
+
5. around_next
|
113
|
+
6. after_next
|
114
|
+
7. before_abort
|
115
|
+
8. before_skip
|
116
|
+
|
117
|
+
Let's extend `Csvbuilder::Import::File`
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
class Importer < Csvbuilder::Import::File
|
121
|
+
attr_reader :row_in_errors
|
122
|
+
|
123
|
+
def initialize(*args)
|
124
|
+
super
|
125
|
+
@row_in_errors = RowErrors.new
|
126
|
+
end
|
127
|
+
|
128
|
+
after_next do
|
129
|
+
next true unless current_row_model # End of File
|
130
|
+
next true if current_row_model.valid? # No Errors To Collect
|
131
|
+
|
132
|
+
row_in_errors.append_errors(current_row_model)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
```
|
136
|
+
|
137
|
+
Now the importer can report the errors encountered instead of ignoring them.
|
138
|
+
|
139
|
+
For documentation purposes, here is a possible implementation of the errors collector:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
class RowErrors
|
143
|
+
attr_reader :headers, :errors
|
144
|
+
|
145
|
+
def initialize
|
146
|
+
@errors = []
|
147
|
+
end
|
148
|
+
|
149
|
+
def append_errors(row_model)
|
150
|
+
@headers ||= begin
|
151
|
+
errors << row_model.class.headers
|
152
|
+
row_model.class.headers
|
153
|
+
end
|
154
|
+
|
155
|
+
row_in_error = []
|
156
|
+
row_model.source_attributes.map do |key, value|
|
157
|
+
row_in_error << if row_model.errors.messages[key].present?
|
158
|
+
"Initial Value: [#{value}] - Errors: #{row_model.errors.messages[key].join(", ")}"
|
159
|
+
else
|
160
|
+
value
|
161
|
+
end
|
162
|
+
end
|
163
|
+
errors << row_in_error
|
164
|
+
end
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
Now we can nicely show the errors which occur over the import.
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
[
|
172
|
+
["First name", "Last name"],
|
173
|
+
["J", "Doe"]
|
174
|
+
]
|
175
|
+
|
176
|
+
importer.row_in_errors.errors
|
177
|
+
# => [
|
178
|
+
# => ["First Name", "Last Name"],
|
179
|
+
# => ["Initial Value: [J] - Errors: is too short (minimum is 2 characters)", "Doe"]
|
180
|
+
# => ]
|
181
|
+
```
|
182
|
+
|
183
|
+
Thanks to the callback mechanism, the opportunities to interact with the import are immense. For instance, you can show the errors on a Web Form and offer the chance to the user to change the data and re-attempt.
|
184
|
+
|
185
|
+
For long imports, you can show a progress bar to help customers cope with the import time; as you know, if errors have occurred, you can change the colour of the progress bar accordingly and offer the possibility to stop the import earlier.
|
186
|
+
|
187
|
+
## Aborting an import
|
188
|
+
|
189
|
+
There is a design challenge to handling an import line-by-line. If it makes the code more efficient and decoupled, we might have cases when we want to check something shared with all lines. The obvious ones are the headers. Let's say we want to check them and abort all imports if something wrong is detected. We probably don't want to add the abort conditioning on every line (Csvbuilder::Model or, more precisely, its extension Csvbuilder::Import). We would rather have it in the Importer itself. In that case, we can stop the Importer from invoking "abort!". Let's consider the following example:
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
class Importer < Csvbuilder::Import::File
|
193
|
+
|
194
|
+
after_next do
|
195
|
+
if HeaderChecker.new(current_row_model).invalid?
|
196
|
+
abort!
|
197
|
+
next true # Keep going into #each and hit the callbacks
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
```
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
context "with incorrect headers" do
|
206
|
+
|
207
|
+
should "not import data" do
|
208
|
+
importer = Importer.new(@file.path, @importer_class, @context)
|
209
|
+
|
210
|
+
row_enumerator = importer.each
|
211
|
+
|
212
|
+
assert_raises StopIteration do
|
213
|
+
row_enumerator.next
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
```
|
218
|
+
|
219
|
+
## References
|
220
|
+
|
221
|
+
- [^1] Csvbuilder::Import::File#each https://github.com/joel/csvbuilder-importer/blob/e8e6633a03dda4ae0e5d6775ec9d395dec553fbe/lib/csvbuilder/importer/public/import/file.rb#L66-L68
|
20
222
|
|
21
223
|
## Development
|
22
224
|
|
@@ -26,7 +228,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
26
228
|
|
27
229
|
## Contributing
|
28
230
|
|
29
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
231
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/joel/csvbuilder-importer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/csvbuilder-importer/blob/main/CODE_OF_CONDUCT.md).
|
30
232
|
|
31
233
|
## License
|
32
234
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
# Runtime dependencies
|
6
|
+
gem "activemodel", github: "rails/rails", branch: "main"
|
7
|
+
gem "activesupport", github: "rails/rails", branch: "main"
|
8
|
+
|
9
|
+
gem "csvbuilder-core"
|
10
|
+
|
11
|
+
# Development dependencies
|
12
|
+
gem "rake"
|
13
|
+
gem "rspec"
|
14
|
+
gem "rubocop"
|
15
|
+
gem "rubocop-performance"
|
16
|
+
gem "rubocop-rake"
|
17
|
+
gem "rubocop-rspec"
|
@@ -20,10 +20,10 @@ module Csvbuilder
|
|
20
20
|
reset
|
21
21
|
end
|
22
22
|
|
23
|
-
#
|
23
|
+
# https://gist.github.com/guilhermesimoes/d69e547884e556c3dc95?permalink_comment_id=4502636#gistcomment-4502636
|
24
24
|
# @return [Integer] the number of rows in the file, including empty new lines
|
25
25
|
def size
|
26
|
-
@size ||=
|
26
|
+
@size ||= ::File.read(file_path).count($/)
|
27
27
|
end
|
28
28
|
|
29
29
|
# If the current position is at the headers, skip it and return it. Otherwise, only return false.
|
@@ -9,7 +9,7 @@ module Csvbuilder
|
|
9
9
|
extend ActiveModel::Callbacks
|
10
10
|
include ActiveModel::Validations
|
11
11
|
|
12
|
-
attr_reader :csv, :row_model_class, :index, :current_row_model, :previous_row_model, :context # -1 = start of file, 0 to infinity = index of row_model, nil = end of file, no row_model
|
12
|
+
attr_reader :interrupt, :csv, :row_model_class, :index, :current_row_model, :previous_row_model, :context # -1 = start of file, 0 to infinity = index of row_model, nil = end of file, no row_model
|
13
13
|
|
14
14
|
delegate :size, :end_of_file?, :line_number, to: :csv
|
15
15
|
|
@@ -26,6 +26,7 @@ module Csvbuilder
|
|
26
26
|
@csv = ::Csvbuilder::Import::Csv.new(file_path) # Full namespace provided to avoid confusion with Ruby CSV class.
|
27
27
|
@row_model_class = row_model_class
|
28
28
|
@context = context.to_h.symbolize_keys
|
29
|
+
@interrupt = false
|
29
30
|
reset
|
30
31
|
end
|
31
32
|
|
@@ -39,6 +40,7 @@ module Csvbuilder
|
|
39
40
|
csv.reset
|
40
41
|
@index = -1
|
41
42
|
@current_row_model = nil
|
43
|
+
@interrupt = false
|
42
44
|
end
|
43
45
|
|
44
46
|
# Gets the next row model based on the context
|
@@ -74,7 +76,13 @@ module Csvbuilder
|
|
74
76
|
|
75
77
|
# @return [Boolean] returns true, if the file should abort reading
|
76
78
|
def abort?
|
77
|
-
!valid? || !!current_row_model.try(:abort?)
|
79
|
+
interrupt || !valid? || !!current_row_model.try(:abort?)
|
80
|
+
end
|
81
|
+
|
82
|
+
def abort!
|
83
|
+
@interrupt = true
|
84
|
+
|
85
|
+
nil
|
78
86
|
end
|
79
87
|
|
80
88
|
# @return [Boolean] returns true, if the file should skip `current_row_model`
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: csvbuilder-importer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joel Azemar
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-07-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -17,9 +17,6 @@ dependencies:
|
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '5.2'
|
20
|
-
- - "<"
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: '8'
|
23
20
|
type: :runtime
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -27,9 +24,6 @@ dependencies:
|
|
27
24
|
- - ">="
|
28
25
|
- !ruby/object:Gem::Version
|
29
26
|
version: '5.2'
|
30
|
-
- - "<"
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '8'
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
28
|
name: activesupport
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -37,9 +31,6 @@ dependencies:
|
|
37
31
|
- - ">="
|
38
32
|
- !ruby/object:Gem::Version
|
39
33
|
version: '5.2'
|
40
|
-
- - "<"
|
41
|
-
- !ruby/object:Gem::Version
|
42
|
-
version: '8'
|
43
34
|
type: :runtime
|
44
35
|
prerelease: false
|
45
36
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -47,9 +38,6 @@ dependencies:
|
|
47
38
|
- - ">="
|
48
39
|
- !ruby/object:Gem::Version
|
49
40
|
version: '5.2'
|
50
|
-
- - "<"
|
51
|
-
- !ruby/object:Gem::Version
|
52
|
-
version: '8'
|
53
41
|
- !ruby/object:Gem::Dependency
|
54
42
|
name: csvbuilder-core
|
55
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -77,12 +65,14 @@ files:
|
|
77
65
|
- CHANGELOG.md
|
78
66
|
- CODE_OF_CONDUCT.md
|
79
67
|
- Gemfile
|
68
|
+
- Gemfile.lock
|
80
69
|
- LICENSE.txt
|
81
70
|
- README.md
|
82
71
|
- Rakefile
|
83
72
|
- gemfiles/Gemfile.5.2._gemfile
|
84
73
|
- gemfiles/Gemfile.6.1._gemfile
|
85
74
|
- gemfiles/Gemfile.7.0._gemfile
|
75
|
+
- gemfiles/rails_edge.gemfile
|
86
76
|
- lib/csvbuilder/importer.rb
|
87
77
|
- lib/csvbuilder/importer/concerns/import/attributes.rb
|
88
78
|
- lib/csvbuilder/importer/concerns/import/base.rb
|
@@ -115,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
105
|
- !ruby/object:Gem::Version
|
116
106
|
version: '0'
|
117
107
|
requirements: []
|
118
|
-
rubygems_version: 3.
|
108
|
+
rubygems_version: 3.4.17
|
119
109
|
signing_key:
|
120
110
|
specification_version: 4
|
121
111
|
summary: Csvbuilder Importer contain the components to handle CSV importing
|