sp2db 0.0.3

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.
@@ -0,0 +1,238 @@
1
+ module Sp2db
2
+ class BaseTable
3
+
4
+ attr_accessor :name,
5
+ :sheet_name,
6
+ :worksheet,
7
+ :find_columns,
8
+ :spreadsheet_id,
9
+ :client
10
+
11
+ def initialize opts={}
12
+
13
+ if opts[:name].blank? && opts[:sheet_name].blank?
14
+ raise "Must specify at least one of name or sheet name"
15
+ end
16
+
17
+ opts.each do |k, v|
18
+ self.send "#{k}=", v
19
+ end
20
+
21
+ self.sheet_name ||= opts[:sheet_name] = config[:sheet_name] || worksheet.try(:title)
22
+ end
23
+
24
+ def active_record?
25
+ false
26
+ end
27
+
28
+ # Table name
29
+ def name
30
+ @name ||= sheet_name.try(:to_sym) || raise("Name cannot be nil")
31
+ end
32
+
33
+ def spreadsheet_id
34
+ @spreadsheet_id ||= config[:spreadsheet_id] || Sp2db.config.spreadsheet_id
35
+ end
36
+
37
+ def name=n
38
+ @name = n&.to_sym
39
+ end
40
+
41
+ def find_columns
42
+ @find_columns ||= config[:find_columns] || Sp2db.config.default_find_columns
43
+ end
44
+
45
+ def required_columns
46
+ @required_columns ||= config[:required_columns] || []
47
+ end
48
+
49
+ def client
50
+ @client = Sp2db.client
51
+ end
52
+
53
+ def spreadsheet
54
+ client.spreadsheet spreadsheet_id
55
+ end
56
+
57
+ def sheet_name
58
+ @sheet_name ||= (config[:sheet_name] || name)&.to_sym
59
+ end
60
+
61
+ def worksheet
62
+ @worksheet = spreadsheet.worksheet_by_name(self.sheet_name.to_s)
63
+ end
64
+
65
+ def sp_data
66
+ retries = 2
67
+ raw_data = CSV.parse worksheet.export_as_string
68
+ data = process_data raw_data, source: :sp
69
+ data
70
+ end
71
+
72
+ def csv_data
73
+ raw_data = CSV.parse File.open(csv_file)
74
+ data = process_data raw_data, source: :csv
75
+ data
76
+ end
77
+
78
+ def header_row
79
+ # @header_row ||= config[:header_row] || 0
80
+ 0
81
+ end
82
+
83
+ def csv_folder
84
+ folder = "#{Sp2db.config.export_location}/csv"
85
+ FileUtils.mkdir_p folder
86
+ folder
87
+ end
88
+
89
+ def csv_file
90
+ "#{csv_folder}/#{name}.csv"
91
+ end
92
+
93
+ def sp_to_csv opts={}
94
+ write_csv to_csv(sp_data)
95
+ end
96
+
97
+ def write_csv data
98
+ File.open csv_file, "wb" do |f|
99
+ f.write data
100
+ end
101
+ csv_file
102
+ end
103
+
104
+ # Array of hash data to csv format
105
+ def to_csv data
106
+ attributes = data.first&.keys || []
107
+
108
+ CSV.generate(headers: true) do |csv|
109
+ csv << attributes
110
+
111
+ data.each do |row|
112
+ csv << attributes.map do |att|
113
+ row[att]
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ # Global config
120
+ def config
121
+ {}.with_indifferent_access
122
+ end
123
+
124
+ def process_data raw_data, opts={}
125
+ raw_data = data_transform raw_data, opts unless opts[:skip_data_transform]
126
+ raw_data = raw_filter raw_data, opts unless opts[:skip_data_filter]
127
+ data = call_process_data raw_data, opts
128
+ data
129
+ end
130
+
131
+
132
+ # Tranform data to standard csv format
133
+ def data_transform raw_data, opts={}
134
+ if config[:data_transform].present?
135
+ config[:data_transform].call *args, &block
136
+ else
137
+ raw_data
138
+ end
139
+ end
140
+
141
+ protected
142
+ # Remove header which starts with "#"
143
+ def valid_header? h
144
+ h.present? && !h.match("^#.*")
145
+ end
146
+
147
+ # Header with "!" at the beginning or ending is required
148
+ def require_header? h
149
+ h.present? && (h.match("^!.*") || h.match(".*?!$"))
150
+ end
151
+
152
+ # Convert number string to number
153
+ def standardize_cell_val v
154
+ v = ((float = Float(v)) && (float % 1.0 == 0) ? float.to_i : float) rescue v
155
+ v = v.force_encoding("UTF-8") if v.is_a?(String)
156
+ v
157
+ end
158
+
159
+ def call_process_data raw_data, opts={}
160
+ data = raw_data
161
+ if (data_proc = config[:process_data]).present?
162
+ data = data_proc.call raw_data
163
+ end
164
+ data
165
+ end
166
+
167
+ # Remove uncessary columns and invalid rows from csv format data
168
+ def raw_filter raw_data, opts={}
169
+ raw_header = raw_data[header_row].map.with_index do |h, idx|
170
+ is_valid = valid_header?(h)
171
+ {
172
+ idx: idx,
173
+ is_remove: !is_valid,
174
+ is_required: require_header?(h),
175
+ name: is_valid && h.gsub(/\s*/, '').gsub(/!/, '').downcase
176
+ }
177
+ end
178
+
179
+ rows = raw_data[(header_row + 1)..-1].map.with_index do |raw, rdx|
180
+ row = {}.with_indifferent_access
181
+ raw_header.each do |h|
182
+ val = raw[h[:idx]]
183
+ next if h[:is_remove]
184
+ if h[:is_required] && val.blank?
185
+ row = {}
186
+ break
187
+ end
188
+
189
+ row[h[:name]] = standardize_cell_val val
190
+ end
191
+
192
+ next if row.values.all?(&:blank?)
193
+
194
+ row[:id] = rdx + 1 if find_columns.include?(:id) && row[:id].blank?
195
+ row
196
+ end.compact
197
+ .reject(&:blank?)
198
+ rows = rows.select do |row|
199
+ if required_columns.present?
200
+ required_columns.all? {|col| row[col].present? }
201
+ else
202
+ true
203
+ end
204
+ end
205
+
206
+ rows
207
+ end
208
+
209
+ class << self
210
+
211
+ def all_tables
212
+ ModelTable.all_tables + NonModelTable.all_tables
213
+ end
214
+
215
+
216
+ def table_by_names *names
217
+ all_tables = self.all_tables
218
+ if names.blank?
219
+ all_tables
220
+ else
221
+ names.map do |n|
222
+ all_tables.find {|tb| tb.name == n.to_sym} || raise("Not found: #{n}")
223
+ end
224
+ end
225
+ end
226
+
227
+ def sp_to_csv *table_names
228
+ table_by_names(*table_names).map(&__method__)
229
+ end
230
+
231
+ def model_table_class
232
+ ModelTable
233
+ end
234
+
235
+ delegate :sp_to_db, :csv_to_db, to: :model_table_class
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,56 @@
1
+ module Sp2db
2
+ class Client
3
+
4
+ include Logging
5
+
6
+ attr_accessor :credential, :session
7
+
8
+ def initialize
9
+
10
+ end
11
+
12
+ def config
13
+ Sp2db.config
14
+ end
15
+
16
+ def credential
17
+ @credential ||= config.credential
18
+ end
19
+
20
+ def session
21
+ logger.debug "Init session"
22
+ unless credential = self.credential
23
+ return @session = saved_session
24
+ end
25
+
26
+ key = OpenSSL::PKey::RSA.new(credential['private_key'])
27
+ auth = Signet::OAuth2::Client.new(
28
+ token_credential_uri: credential['token_uri'],
29
+ audience: credential['token_uri'],
30
+ scope: %w(
31
+ https://www.googleapis.com/auth/drive
32
+ https://spreadsheets.google.com/feeds/
33
+ ),
34
+ issuer: credential['client_email'],
35
+ signing_key: key
36
+ )
37
+
38
+ auth.fetch_access_token!
39
+ @session = GoogleDrive.login_with_oauth(auth.access_token)
40
+ end
41
+
42
+
43
+ def saved_session
44
+ logger.debug "Use saved session"
45
+ GoogleDrive.saved_session Sp2db.config.personal_credential,
46
+ nil,
47
+ client_id: config.client_id,
48
+ client_secret: config.client_secret
49
+ end
50
+
51
+ def spreadsheet sid
52
+ Spreadsheet.new session.spreadsheet_by_key(sid)
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,82 @@
1
+ module Sp2db
2
+ class Config
3
+
4
+ attr_accessor \
5
+ :credential,
6
+ :personal_credential,
7
+ :client_id,
8
+ :client_secret,
9
+ :spreadsheet_id,
10
+ :export_location,
11
+ :default_file_extention,
12
+ :import_strategy,
13
+ :download_before_import,
14
+ :default_extensions,
15
+ :exception_handler,
16
+ :non_model_tables,
17
+ :default_find_columns
18
+
19
+
20
+ DEFAULT = {
21
+ personal_credential: "credentials/google_credentials.json",
22
+ import_strategy: :truncate_all,
23
+ export_location: "db/spreadsheets",
24
+ default_file_extention: :csv,
25
+ exception_handler: OpenStruct.new({
26
+ row_import_error: :raise,
27
+ table_import_error: :raise,
28
+ }),
29
+ non_model_tables: {}.with_indifferent_access,
30
+ download_before_import: false,
31
+ default_extensions: :csv,
32
+ default_find_columns: [:id],
33
+ }
34
+
35
+ SUPPORTED_EXTENSIONS = [:csv]
36
+
37
+ def initialize
38
+ set_default
39
+ end
40
+
41
+ def import_strategy=s
42
+ s = s.to_sym
43
+ ImportStrategy.valid! s
44
+ @import_strategy = s
45
+ end
46
+
47
+ def export_folder
48
+ FileUtils.mkdir_p export_location
49
+ export_location
50
+ end
51
+
52
+ # File name or json string or hash
53
+ def credential=cr
54
+ if File.exist?(cr) && File.file?(cr)
55
+ cr = File.read cr
56
+ end
57
+
58
+ @credential = case cr
59
+ when Hash, ActiveSupport::HashWithIndifferentAccess
60
+ cr
61
+ when String
62
+ JSON.parse cr
63
+ else
64
+ raise "Invalid data type"
65
+ end
66
+ end
67
+
68
+ def default_find_columns= cols
69
+ @default_find_columns = cols.map &:to_sym
70
+ end
71
+
72
+ private
73
+
74
+ Google::Apis.logger.level
75
+
76
+ def set_default
77
+ DEFAULT.each do |k, v|
78
+ self.send("#{k}=", v)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,26 @@
1
+ module Sp2db
2
+ module ExceptionHandler
3
+ extend self
4
+
5
+ def row_import_error e
6
+ handle __method__, e
7
+ end
8
+
9
+ def table_import_error e
10
+ handle __method__, e
11
+ end
12
+
13
+ def handle action, e
14
+ case action = Sp2db.config.exception_handler[action]
15
+ when :skip
16
+ true
17
+ when :raise
18
+ raise e
19
+ when Proc
20
+ action.call(e)
21
+ else
22
+ raise e
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,84 @@
1
+ module Sp2db
2
+ module ImportConcern
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ Sp2db::ModelTable.add_models self
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ def sp2db_options *args, &block
12
+ if args.first.is_a?(Hash)
13
+ args.first.each do |k, v|
14
+ send "sp2db_#{k}", v
15
+ end
16
+ else
17
+ meth = args.shift
18
+ send "sp2db_#{meth}", *args, &block
19
+ end
20
+ end
21
+
22
+ def sp2db_config
23
+ @sp2db_config ||= {}.with_indifferent_access
24
+ end
25
+
26
+ [:find_columns, :required_columns].each do |opt|
27
+ define_method "sp2db_#{opt}" do |*cols|
28
+ cols = cols&.flatten
29
+ sp2db_config[opt] = cols.map(&:to_sym) if cols.present?
30
+ sp2db_config[opt]
31
+ end
32
+ end
33
+
34
+ def sp2db_priority pr=nil
35
+ sp2db_config[:priority] = pr if pr.present?
36
+ sp2db_config[:priority]
37
+ end
38
+
39
+ def sp2db_import_strategy s=nil
40
+ if s.present?
41
+ s = s.to_sym
42
+ ImportStrategy.valid! s
43
+ sp2db_config[:import_strategy] = s
44
+ end
45
+
46
+ sp2db_config[:import_strategy]
47
+ end
48
+
49
+ def sp2db_sheet_name s=nil
50
+ sp2db_config[:sheet_name] = s.to_sym if s.present?
51
+ sp2db_config[:sheet_name]
52
+ end
53
+
54
+ def sp2db_header_row s=nil
55
+ sp2db_config[:header_row] = s if s.present?
56
+ sp2db_config[:header_row]
57
+ end
58
+
59
+ def sp2db_spreadsheet_id s=nil
60
+ sp2db_config[:spreadsheet_id] = s if s.present?
61
+ sp2db_config[:spreadsheet_id]
62
+ end
63
+
64
+ [
65
+ :data_transform,
66
+ :process_data,
67
+ :before_import_row,
68
+ :after_import_row,
69
+ :after_import_table,
70
+ ].each do |option|
71
+ define_method "sp2db_#{option}" do |method=nil, &block|
72
+ sp2db_config[option] = if method.present?
73
+ method.is_a?(Proc) ? method : method.to_sym
74
+ elsif block.present?
75
+ block
76
+ else
77
+ sp2db_config[option]
78
+ end
79
+ end
80
+ end
81
+
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,157 @@
1
+ module Sp2db
2
+ module ImportStrategy
3
+
4
+ extend self
5
+
6
+ # Add strategy
7
+ def add label, strategy=nil, &block
8
+ strategy ||= Class.new(Base)
9
+ strategy.class_eval(&block) if block_given?
10
+ strategies[label.to_sym] = strategy
11
+ end
12
+
13
+ # @!strategies [ro] strategies
14
+ def strategies
15
+ @strategies ||= {}.with_indifferent_access
16
+ end
17
+
18
+ def strategy_by_name name
19
+ strategies[name.to_s] || raise("Invalid import strategy: #{name}")
20
+ end
21
+
22
+ def labels
23
+ strategies.keys.map(&:to_sym)
24
+ end
25
+
26
+ def valid! s
27
+ raise "Unsuported strategies" unless labels.include?(s.to_sym)
28
+ true
29
+ end
30
+
31
+ class Base
32
+
33
+ include Logging
34
+
35
+ attr_accessor :table, :rows, :result
36
+
37
+ delegate :model,
38
+ :find_columns,
39
+ to: :table
40
+
41
+ def initialize table, rows
42
+ self.table = table
43
+ self.rows = rows
44
+ end
45
+
46
+ def result
47
+ @result ||= {
48
+ records: [],
49
+ errors: [],
50
+ }.with_indifferent_access
51
+ end
52
+
53
+ def errors
54
+ result[:errors]
55
+ end
56
+
57
+ def records
58
+ result[:records]
59
+ end
60
+
61
+ def before_import
62
+ logger.debug "Run before import table: #{self.table.name}"
63
+ end
64
+
65
+ def find_db_row row
66
+ if find_columns.present?
67
+ cond = {}
68
+ find_columns.each do |col|
69
+ cond[col] = row[col]
70
+ end
71
+ model.find_by cond
72
+ else
73
+ nil # nil to skip
74
+ end
75
+ end
76
+
77
+ def set_record_value record, row
78
+ row.each do |k, v|
79
+ record.send("#{k}=", v)
80
+ end
81
+ record
82
+ end
83
+
84
+ def import_row row
85
+ record = find_db_row(row) || model.new(row)
86
+ record = set_record_value record, row
87
+ return unless record.present?
88
+ record.save! if record.new_record? || record.changed?
89
+ record
90
+ end
91
+
92
+ def after_import
93
+ logger.debug "Run after import table: #{self.table.name}"
94
+ end
95
+
96
+ def import
97
+ logger.debug "Start import table: #{self.table.name}"
98
+ ActiveRecord::Base.transaction(requires_new: true) do
99
+ before_import
100
+ rows.each do |row|
101
+ row = row.clone
102
+ begin
103
+ table.before_import_row row
104
+ record = import_row row
105
+ records << record
106
+ table.after_import_row record
107
+ rescue ActiveRecord::ActiveRecordError => e
108
+ logger.error e.try(:message)
109
+ errors << {
110
+ message: e.try(:message),
111
+ exception: e,
112
+ row: row,
113
+ table: table.name,
114
+ }
115
+ next unless ExceptionHandler.row_import_error e
116
+ end
117
+ end
118
+ after_import
119
+ table.after_import_table result
120
+ logger.debug "Import finished: #{self.table.name}"
121
+ return result
122
+ end
123
+
124
+ end
125
+
126
+ end
127
+ end
128
+
129
+ ImportStrategy.add :truncate_all do
130
+ def before_import
131
+ logger.info "Truncte all data: #{self.table.name}"
132
+ model.all.delete_all
133
+ end
134
+
135
+ def find_db_row row
136
+ nil
137
+ end
138
+ end
139
+
140
+ ImportStrategy.add :overwrite do
141
+ end
142
+
143
+ ImportStrategy.add :fill_empty do
144
+ def set_record_value record, row
145
+ row.each do |k, v|
146
+ record.send("#{k}=", v) if record.send(k).blank?
147
+ end
148
+ record
149
+ end
150
+ end
151
+
152
+ ImportStrategy.add :skip do
153
+ def set_record_value record, row
154
+ record
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,29 @@
1
+ module Sp2db
2
+ module Logging
3
+
4
+ extend self
5
+
6
+ # @!attribute [rw] logger
7
+ # @return [Logger] The logger.
8
+ def logger
9
+ @logger ||= get_logger
10
+ end
11
+
12
+ # @return [Logger]
13
+ def logger= l
14
+ @logger = l
15
+ end
16
+
17
+ # @return [Logger]
18
+ def get_logger
19
+ if defined?(::Rails) && ::Rails.respond_to?(:logger) && !::Rails.logger.nil?
20
+ ::Rails.logger
21
+ else
22
+ logger = Logger.new($stdout)
23
+ logger.level = Logger::WARN
24
+ logger
25
+ end
26
+ end
27
+
28
+ end
29
+ end