csv_party 1.0.0.rc4 → 1.0.0.rc5
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 +5 -5
- data/ROADMAP.md +84 -0
- data/lib/csv_party/data_preparer.rb +4 -1
- data/lib/csv_party/dsl.rb +2 -0
- data/lib/csv_party/parsers.rb +3 -1
- data/lib/csv_party/row.rb +5 -4
- data/lib/csv_party/runner.rb +28 -61
- data/lib/csv_party/validations.rb +41 -0
- data/lib/csv_party.rb +0 -6
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 174fc0ba77e52795b1763e2ff29eae5e5c3cfa3bcd181973aaba886a54a79eeb
|
4
|
+
data.tar.gz: 7676780e13435e10bf35b7c34c849484ea0efe0a86d7889ff2a4697f312a99c6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ab4bc476717516d1fa2a9d674f47e60c95ef0b24541cb7cf6cf76948ca5b9ce9dad96b9b89d10edfd99f338d4a04b612fd241c38d37d746d049f6eedb8868120
|
7
|
+
data.tar.gz: 4041355ba68669a3cf3e6275bf099da83d3d92d759bfc328269080a41dcbee02fa9a77b9c95c3549eaf5330cfc9fbbdcfdea05f83d320d2f29b55e89553d3fea
|
data/ROADMAP.md
CHANGED
@@ -8,9 +8,13 @@ Roadmap
|
|
8
8
|
- [1.5 Runtime Configuration](#15-runtime-configuration)
|
9
9
|
- [1.6 CSV Parse Error Handling](#16-csv-parse-error-handling)
|
10
10
|
- [Someday Features](#someday-features)
|
11
|
+
- [Parse Row Access](#parse-row-access)
|
12
|
+
- [Deferred Parsing](#deferred-parsing)
|
13
|
+
- [Columns Macro](#columns-macro)
|
11
14
|
- [Column Numbers](#column-numbers)
|
12
15
|
- [Multi-column Parsing](#multi-column-parsing)
|
13
16
|
- [Parse Dependencies](#parse-dependencies)
|
17
|
+
- [Rails Generator](#rails-generator0
|
14
18
|
|
15
19
|
#### 1.1 Early Return While Parsing
|
16
20
|
|
@@ -230,6 +234,68 @@ error handling API for non-parse errors. So:
|
|
230
234
|
|
231
235
|
## Someday Features
|
232
236
|
|
237
|
+
#### Parse Row Access
|
238
|
+
|
239
|
+
This feature would allow access to the `CSV::Row` object when parsing a column.
|
240
|
+
It could work something like this:
|
241
|
+
|
242
|
+
column product do |value, row|
|
243
|
+
Product.find_by(name: row['Product'])
|
244
|
+
end
|
245
|
+
|
246
|
+
In theory, the CSVParty API would cover all use cases where somebody would need
|
247
|
+
to access the raw row data, but perhaps not. Sometimes it's nice to be able to
|
248
|
+
cut through the stuff in your way and just get at the raw internals.
|
249
|
+
|
250
|
+
Additionally, perhaps deferred parsing would allow access to parsed row values,
|
251
|
+
which would possibly enable some of the features below, Multi Column Parsing and
|
252
|
+
Parse Dependencies, without requiring additional code. That might look like:
|
253
|
+
|
254
|
+
column product do |value, row|
|
255
|
+
Product.find_by(name: row.unparsed.row)
|
256
|
+
end
|
257
|
+
|
258
|
+
#### Deferred Parsing
|
259
|
+
|
260
|
+
Currently, CSVParty parses all columns before the row import logic is executed.
|
261
|
+
There are situations where parsing columns is expensive and you may want to
|
262
|
+
defer parsing columns until and unless you actually need them for that a given
|
263
|
+
row. For example, say you have an import where you are either:
|
264
|
+
|
265
|
+
1. Updating a value on existing records, or
|
266
|
+
2. Setting a value on and creating new records
|
267
|
+
|
268
|
+
And when you create new records, you have to also set a bunch of other values
|
269
|
+
and some of those values require database queries.
|
270
|
+
|
271
|
+
In a situation like this, you would want to defer all of the database queries
|
272
|
+
that need to run when creating a new record so that they aren't done in cases
|
273
|
+
where you are updating an existing record.
|
274
|
+
|
275
|
+
#### Columns Macro
|
276
|
+
|
277
|
+
This feature would allow you to declare multiple columns in a single line. So,
|
278
|
+
rather than:
|
279
|
+
|
280
|
+
column :product
|
281
|
+
column :price
|
282
|
+
column :color
|
283
|
+
|
284
|
+
You could do:
|
285
|
+
|
286
|
+
columns :product, :price, :color
|
287
|
+
|
288
|
+
This is probably most useful when there are a bunch of columns that should all
|
289
|
+
be parsed as text. Though it might make sense to allow specifying parsers and
|
290
|
+
other options:
|
291
|
+
|
292
|
+
columns product: { as: :raw }, price: { as: :decimal }
|
293
|
+
|
294
|
+
It should probably also be possible to combine `columns` and `column` macros:
|
295
|
+
|
296
|
+
columns :product, :price, :color
|
297
|
+
column :size
|
298
|
+
|
233
299
|
#### Column Numbers
|
234
300
|
|
235
301
|
CSVParty is entirely oriented around a CSV file having a header. This is not
|
@@ -269,3 +335,21 @@ that might look like:
|
|
269
335
|
customer.orders.find(order_id)
|
270
336
|
end
|
271
337
|
end
|
338
|
+
|
339
|
+
#### Rails Generator
|
340
|
+
|
341
|
+
This feature would add a generator for Rails which creates an importer file. So
|
342
|
+
doing:
|
343
|
+
|
344
|
+
rails generate importer Product
|
345
|
+
|
346
|
+
Would generate the following file at `app/importers/product_importer.rb`:
|
347
|
+
|
348
|
+
class ProductImporter
|
349
|
+
include CSVParty
|
350
|
+
|
351
|
+
import do |row|
|
352
|
+
# import logic goes here
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'csv_party/errors'
|
3
|
+
|
1
4
|
module CSVParty
|
2
5
|
class DataPreparer
|
3
6
|
def initialize(options)
|
@@ -15,7 +18,7 @@ module CSVParty
|
|
15
18
|
@options[:content]
|
16
19
|
end
|
17
20
|
options = extract_csv_options.merge(headers: true)
|
18
|
-
CSV.new(data, options)
|
21
|
+
CSV.new(data, **options)
|
19
22
|
end
|
20
23
|
|
21
24
|
private
|
data/lib/csv_party/dsl.rb
CHANGED
data/lib/csv_party/parsers.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
1
3
|
module CSVParty
|
2
4
|
module Parsers
|
3
5
|
def parse_raw(value)
|
@@ -25,7 +27,7 @@ module CSVParty
|
|
25
27
|
end
|
26
28
|
|
27
29
|
def parse_decimal(value)
|
28
|
-
BigDecimal
|
30
|
+
BigDecimal(prepare_numeric_value(value))
|
29
31
|
end
|
30
32
|
|
31
33
|
def parse_date(value, format = nil)
|
data/lib/csv_party/row.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
1
3
|
module CSVParty
|
2
4
|
class Row
|
3
5
|
attr_accessor :row_number, :csv_string, :unparsed
|
4
6
|
|
5
|
-
def initialize(csv_row,
|
7
|
+
def initialize(csv_row, runner)
|
6
8
|
@csv_row = csv_row
|
7
|
-
@config = config
|
8
9
|
@runner = runner
|
9
10
|
@attributes = OpenStruct.new
|
10
11
|
parse_row!(csv_row)
|
@@ -16,7 +17,7 @@ module CSVParty
|
|
16
17
|
self.csv_string = csv_row.to_csv
|
17
18
|
self.unparsed = extract_unparsed_values(csv_row)
|
18
19
|
|
19
|
-
@config.columns.each do |column, options|
|
20
|
+
@runner.config.columns.each do |column, options|
|
20
21
|
header = options[:header]
|
21
22
|
value = csv_row[header]
|
22
23
|
@attributes[column] = parse_value(value, options)
|
@@ -25,7 +26,7 @@ module CSVParty
|
|
25
26
|
|
26
27
|
def extract_unparsed_values(csv_row)
|
27
28
|
unparsed_row = OpenStruct.new
|
28
|
-
config.columns.each do |column, options|
|
29
|
+
@runner.config.columns.each do |column, options|
|
29
30
|
header = options[:header]
|
30
31
|
unparsed_row[column] = csv_row[header]
|
31
32
|
end
|
data/lib/csv_party/runner.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
require 'csv_party/parsers'
|
2
|
+
require 'csv_party/validations'
|
3
|
+
require 'csv_party/errors'
|
4
|
+
require 'csv_party/row'
|
2
5
|
|
3
6
|
module CSVParty
|
4
7
|
class Runner
|
5
8
|
include Parsers
|
9
|
+
include Validations
|
6
10
|
|
7
11
|
attr_accessor :csv, :config, :importer
|
8
12
|
|
@@ -52,24 +56,23 @@ module CSVParty
|
|
52
56
|
csv.rewind
|
53
57
|
end
|
54
58
|
|
59
|
+
def initialize_regex_headers!
|
60
|
+
config.columns_with_regex_headers.each do |name, options|
|
61
|
+
found_header = @_headers.find do |header|
|
62
|
+
options[:header].match(header)
|
63
|
+
end
|
64
|
+
options[:header] = found_header || name.to_s
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
55
68
|
def import_rows!
|
56
69
|
loop do
|
57
70
|
begin
|
58
|
-
|
59
|
-
break unless
|
60
|
-
import_row!(
|
61
|
-
rescue NextRowError
|
62
|
-
next
|
63
|
-
rescue SkippedRowError => error
|
64
|
-
handle_skipped_row(error)
|
65
|
-
rescue AbortedRowError => error
|
66
|
-
handle_aborted_row(error)
|
67
|
-
rescue AbortedImportError
|
68
|
-
raise
|
71
|
+
csv_row = csv.shift
|
72
|
+
break unless csv_row
|
73
|
+
import_row!(csv_row)
|
69
74
|
rescue CSV::MalformedCSVError
|
70
75
|
raise
|
71
|
-
rescue StandardError => error
|
72
|
-
handle_error(error, @_current_row_number, row.to_csv)
|
73
76
|
end
|
74
77
|
end
|
75
78
|
|
@@ -78,9 +81,20 @@ module CSVParty
|
|
78
81
|
|
79
82
|
def import_row!(csv_row)
|
80
83
|
@_current_row_number += 1
|
81
|
-
@_current_parsed_row = Row.new(csv_row,
|
84
|
+
@_current_parsed_row = Row.new(csv_row, self)
|
82
85
|
@_current_parsed_row.row_number = @_current_row_number
|
86
|
+
|
83
87
|
instance_exec(@_current_parsed_row, &config.row_importer)
|
88
|
+
rescue NextRowError
|
89
|
+
return
|
90
|
+
rescue SkippedRowError => error
|
91
|
+
handle_skipped_row(error)
|
92
|
+
rescue AbortedRowError => error
|
93
|
+
handle_aborted_row(error)
|
94
|
+
rescue AbortedImportError
|
95
|
+
raise
|
96
|
+
rescue StandardError => error
|
97
|
+
handle_error(error, @_current_row_number, csv_row.to_csv)
|
84
98
|
end
|
85
99
|
|
86
100
|
def next_row!
|
@@ -138,38 +152,6 @@ module CSVParty
|
|
138
152
|
.new(error, line_number, csv_string)
|
139
153
|
end
|
140
154
|
|
141
|
-
def raise_unless_row_processor_is_defined!
|
142
|
-
return if config.row_importer
|
143
|
-
|
144
|
-
raise UndefinedRowProcessorError.new
|
145
|
-
end
|
146
|
-
|
147
|
-
def raise_unless_rows_have_been_imported!
|
148
|
-
return if @_rows_have_been_imported
|
149
|
-
|
150
|
-
raise UnimportedRowsError.new
|
151
|
-
end
|
152
|
-
|
153
|
-
def raise_unless_all_dependencies_are_present!
|
154
|
-
config.dependencies.each do |dependency|
|
155
|
-
next unless importer.send(dependency).nil?
|
156
|
-
|
157
|
-
raise MissingDependencyError.new(self, dependency)
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
|
-
# This error has to be raised at runtime because, when the class body
|
162
|
-
# is being executed, the parser methods won't be available unless
|
163
|
-
# they are defined above the column definitions in the class body
|
164
|
-
def raise_unless_all_named_parsers_exist!
|
165
|
-
config.columns_with_named_parsers.each do |name, options|
|
166
|
-
parser = options[:parser]
|
167
|
-
next if named_parsers.include? parser
|
168
|
-
|
169
|
-
raise UnknownParserError.new(name, parser, named_parsers)
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
155
|
def named_parsers
|
174
156
|
(methods +
|
175
157
|
private_methods +
|
@@ -177,21 +159,6 @@ module CSVParty
|
|
177
159
|
importer.private_methods).grep(/^parse_/)
|
178
160
|
end
|
179
161
|
|
180
|
-
def raise_unless_csv_has_all_columns!
|
181
|
-
return if missing_columns.empty?
|
182
|
-
|
183
|
-
raise MissingColumnError.new(present_columns, missing_columns)
|
184
|
-
end
|
185
|
-
|
186
|
-
def initialize_regex_headers!
|
187
|
-
config.columns_with_regex_headers.each do |name, options|
|
188
|
-
found_header = @_headers.find do |header|
|
189
|
-
options[:header].match(header)
|
190
|
-
end
|
191
|
-
options[:header] = found_header || name.to_s
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
162
|
def respond_to_missing?(method, _include_private)
|
196
163
|
importer.respond_to?(method, true)
|
197
164
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module CSVParty
|
2
|
+
module Validations
|
3
|
+
def raise_unless_row_processor_is_defined!
|
4
|
+
return if config.row_importer
|
5
|
+
|
6
|
+
raise UndefinedRowProcessorError.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def raise_unless_rows_have_been_imported!
|
10
|
+
return if @_rows_have_been_imported
|
11
|
+
|
12
|
+
raise UnimportedRowsError.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def raise_unless_all_dependencies_are_present!
|
16
|
+
config.dependencies.each do |dependency|
|
17
|
+
next unless importer.send(dependency).nil?
|
18
|
+
|
19
|
+
raise MissingDependencyError.new(self, dependency)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# This error has to be raised at runtime because, when the class body
|
24
|
+
# is being executed, the parser methods won't be available unless
|
25
|
+
# they are defined above the column definitions in the class body
|
26
|
+
def raise_unless_all_named_parsers_exist!
|
27
|
+
config.columns_with_named_parsers.each do |name, options|
|
28
|
+
parser = options[:parser]
|
29
|
+
next if named_parsers.include? parser
|
30
|
+
|
31
|
+
raise UnknownParserError.new(name, parser, named_parsers)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def raise_unless_csv_has_all_columns!
|
36
|
+
return if missing_columns.empty?
|
37
|
+
|
38
|
+
raise MissingColumnError.new(present_columns, missing_columns)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/csv_party.rb
CHANGED
@@ -1,11 +1,5 @@
|
|
1
|
-
require 'bigdecimal'
|
2
|
-
require 'csv'
|
3
|
-
require 'ostruct'
|
4
|
-
require 'csv_party/configuration'
|
5
1
|
require 'csv_party/dsl'
|
6
2
|
require 'csv_party/data_preparer'
|
7
|
-
require 'csv_party/errors'
|
8
|
-
require 'csv_party/row'
|
9
3
|
require 'csv_party/runner'
|
10
4
|
|
11
5
|
module CSVParty
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: csv_party
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.
|
4
|
+
version: 1.0.0.rc5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rico Jones
|
@@ -28,6 +28,7 @@ files:
|
|
28
28
|
- lib/csv_party/row.rb
|
29
29
|
- lib/csv_party/runner.rb
|
30
30
|
- lib/csv_party/testing.rb
|
31
|
+
- lib/csv_party/validations.rb
|
31
32
|
homepage: https://github.com/toasterlovin/csv_party
|
32
33
|
licenses:
|
33
34
|
- MIT
|
@@ -40,15 +41,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
40
41
|
requirements:
|
41
42
|
- - ">="
|
42
43
|
- !ruby/object:Gem::Version
|
43
|
-
version: '2.
|
44
|
+
version: '2.6'
|
44
45
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
46
|
requirements:
|
46
47
|
- - ">"
|
47
48
|
- !ruby/object:Gem::Version
|
48
49
|
version: 1.3.1
|
49
50
|
requirements: []
|
50
|
-
|
51
|
-
rubygems_version: 2.6.11
|
51
|
+
rubygems_version: 3.0.3.1
|
52
52
|
signing_key:
|
53
53
|
specification_version: 4
|
54
54
|
summary: CSV Party
|