sp2db 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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