jcangas-datagateway 1.2.2 → 1.4.4

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.
Files changed (48) hide show
  1. data/VERSION.yml +2 -2
  2. data/lib/datagateway/conduits/csv_conduit.rb +0 -0
  3. data/lib/datagateway/conduits/yaml_conduit.rb +0 -0
  4. data/lib/datagateway/data_gateway.rb +27 -514
  5. data/lib/datagateway/dbchange_point.rb +0 -0
  6. data/lib/datagateway/encoder.rb +27 -0
  7. data/lib/datagateway/folder_writer.rb +0 -0
  8. data/lib/datagateway/job.rb +223 -0
  9. data/lib/datagateway/jobs/nexus_export.rb +7 -6
  10. data/lib/datagateway/jobs/nexus_import.rb +1 -4
  11. data/lib/datagateway/jobs/shopin_export.rb +9 -6
  12. data/lib/datagateway/jobs/shopin_import.rb +9 -8
  13. data/lib/datagateway/mapper.rb +226 -0
  14. data/lib/datagateway/nexus/nax_ar.rb +0 -0
  15. data/lib/datagateway/nexus/nax_tlb.rb +0 -0
  16. data/lib/datagateway/nexus/nexus_ar.rb +0 -0
  17. data/lib/datagateway/ssh_transfer.rb +0 -0
  18. data/lib/datagateway.rb +111 -77
  19. data/lib/version.rb +0 -0
  20. data/template-prj/{README → nexus/README} +0 -0
  21. data/template-prj/{config → nexus/config}/README +0 -0
  22. data/template-prj/nexus/config/database.yml +17 -0
  23. data/template-prj/{config → nexus/config}/settings.yml +0 -2
  24. data/template-prj/{data → nexus/data}/donebox/README +0 -0
  25. data/template-prj/{data → nexus/data}/inbox/README +0 -0
  26. data/template-prj/{data → nexus/data}/outbox/README +0 -0
  27. data/template-prj/nexus/export-data.rb +9 -0
  28. data/template-prj/nexus/import-data.rb +14 -0
  29. data/template-prj/{log → nexus/log}/README +0 -0
  30. data/template-prj/{ruby.exe → nexus/ruby.exe} +0 -0
  31. data/template-prj/nexus/sistema.ini +4 -0
  32. data/template-prj/shopin/README +2 -0
  33. data/template-prj/shopin/config/README +4 -0
  34. data/template-prj/shopin/config/database.yml +8 -0
  35. data/template-prj/shopin/config/settings.yml +13 -0
  36. data/template-prj/shopin/data/donebox/README +1 -0
  37. data/template-prj/shopin/data/inbox/README +1 -0
  38. data/template-prj/shopin/data/outbox/README +1 -0
  39. data/template-prj/shopin/export-data.rb +6 -0
  40. data/template-prj/shopin/import-data.rb +6 -0
  41. data/template-prj/shopin/log/README +2 -0
  42. data/test/datagateway_test.rb +0 -0
  43. data/test/test_helper.rb +0 -0
  44. metadata +41 -20
  45. data/template-prj/config/database.yml +0 -32
  46. data/template-prj/dgw-job.rb +0 -25
  47. data/template-prj/run.bat +0 -1
  48. data/template-prj/sistema.ini +0 -3
@@ -0,0 +1,223 @@
1
+ require 'encoder'
2
+ require 'mapper'
3
+
4
+ module DataGateway
5
+ class Job
6
+ attr_accessor :format_id
7
+ attr :name
8
+
9
+ def initialize(name, &block)
10
+ @name = name
11
+ @use_resources = DataGateway.use_resources
12
+ @encoder = NullEncoder.new
13
+ instance_eval(&block) if block_given?
14
+ end
15
+
16
+ def run
17
+ DataGateway.logger.info "running job #{name}"
18
+ do_run
19
+ DataGateway.logger.info "job #{name} finished"
20
+ end
21
+
22
+ private
23
+ def use_resources(bool)
24
+ @use_resources = bool
25
+ end
26
+
27
+ def encode(options)
28
+ @encoder = Encoder.new(options)
29
+ end
30
+
31
+ def pack(data)
32
+ DataGateway.conduit_for(self.format_id).pack(data)
33
+ end
34
+
35
+ def unpack(data)
36
+ DataGateway.conduit_for(format_id).unpack(data)
37
+ end
38
+
39
+ def dbchange_point
40
+ @dbchange_point ||= DBChangePoint.class_for(ActiveRecord::Base.connection).new
41
+ end
42
+ end
43
+
44
+ class Import < Job
45
+ def initialize(name, &block)
46
+ @mappers = {} #import mappers
47
+ super
48
+ end
49
+
50
+ def do_run
51
+ new_last_time = Time.now
52
+ Dir.glob(File.join(DataGateway::inbox, '*.zip')).sort.each do |filename|
53
+ DataGateway.logger.info "Importing #{filename}"
54
+ import_file(filename)
55
+ FileUtils.mv(filename, DataGateway::donebox)
56
+ DataGateway.logger.info "Import done for #{filename}"
57
+ end
58
+ DataGateway.last_time['import'] = new_last_time
59
+ end
60
+
61
+ def donebox
62
+ Dir.glob(File.join(DataGateway::donebox, '*.zip')).each do |filename|
63
+ FileUtils.rm(filename) if File.mtime(filename) < 7.days.ago
64
+ end
65
+ end
66
+
67
+ def get_mapper(table)
68
+ @mappers[table] ||= TableMapper.new(self, table)
69
+ end
70
+
71
+ public # DSL
72
+
73
+ def importing(table, &block)
74
+ get_mapper(table).instance_eval(&block) if block_given?
75
+ end
76
+
77
+ def import_file(file_name)
78
+ self.format_id = file_name
79
+ clear_fixups
80
+ begin
81
+ Zip::ZipInputStream::open(file_name) { |io|
82
+ while (entry = io.get_next_entry)
83
+ content = io.read
84
+ fname = entry.name
85
+ if fname =~ /^attach\//
86
+ fname = fname.gsub(/^attach\//, '')
87
+ import_attach(fname, content)
88
+ else
89
+ class_name = File.basename(fname, '.*').downcase.camelize.to_sym
90
+ import_class(class_name, content)
91
+ end
92
+ end
93
+ }
94
+ ensure
95
+ solve_fixups
96
+ end
97
+ end
98
+
99
+ def clear_fixups
100
+ @mappers.each_value {|mapper| mapper.clear_fixups }
101
+ end
102
+
103
+ def solve_fixups
104
+ @mappers.each_value {|mapper| mapper.solve_fixups }
105
+ end
106
+
107
+ def import_class(class_name, data)
108
+ DataGateway.logger.info "Importing data for #{class_name}"
109
+ mapper = get_mapper(class_name)
110
+ if mapper.ignore?
111
+ DataGateway.logger.info "Ignored"
112
+ return
113
+ end
114
+ mapper.update_all unpack(data)
115
+ #p dbchange_point.capture_for(klass)
116
+ DataGateway.logger.info "done for #{class_name}"
117
+ end
118
+
119
+ def import_attach(fname, content)
120
+ return unless @use_resources
121
+ DataGateway.logger.info "importing attach #{fname}"
122
+ target_file = File.expand_path(fname, DataGateway.resource_path)
123
+ FileUtils.mkdir_p(File.dirname(target_file))
124
+ File.open(target_file, "wb") {|f| f.write content}
125
+ end
126
+ end
127
+
128
+ class Export < Job
129
+ def initialize(name, &block)
130
+ @exports = {}
131
+ @exports_order = []
132
+ export_to :csv #default format
133
+ super
134
+ end
135
+
136
+ def each
137
+ tables = @exports_order
138
+ tables.each { |cname|
139
+ yield cname, export_class(cname)
140
+ }
141
+ end
142
+
143
+ def export_folder
144
+ @export_folder ||= create_folder
145
+ end
146
+
147
+ def close_export_folder
148
+ @export_folder.close if @export_folder
149
+ end
150
+
151
+ def do_run
152
+ new_last_time = Time.now
153
+ begin
154
+ self.each { |cname, records|
155
+ file_name = "#{cname}.#{self.format_id}"
156
+ DataGateway.logger.debug "Exporting archive #{file_name}"
157
+ write_file(file_name, records)
158
+ }
159
+ ensure
160
+ close_export_folder
161
+ end
162
+ DataGateway.last_time['export'] = new_last_time
163
+ end
164
+
165
+ private # DSL
166
+
167
+ def exporting(table_name, options = {})
168
+ @exports_order << table_name
169
+ @exports[table_name] = options
170
+ end
171
+
172
+ def export_to(format)
173
+ self.format_id = format
174
+ end
175
+
176
+ private
177
+ def create_folder(outbox = DataGateway::outbox)
178
+ FileUtils.mkpath(outbox)
179
+ stamp = Time.now.strftime("%Y%m%d%H%M%S")
180
+ folder_name = File.join(outbox, "#{stamp}.#{self.format_id}")
181
+ #(APP_ENV == 'development') ? FolderWriter.new(folder_name) :
182
+ ZipFolderWriter.new(folder_name)
183
+ end
184
+
185
+ def export_class(cname)
186
+ result = []
187
+ bm = Benchmark.measure {
188
+ klass = DataGateway.model_for(cname.to_s.downcase.camelize)
189
+ DataGateway.logger.info "Exporting #{klass} (#{self.format_id})"
190
+ records = klass.find(:all, @exports[cname])
191
+ records.each { |r| export_attachments(r) } if @use_resources
192
+ result = pack(records) unless records.empty?
193
+ }
194
+ DataGateway.logger.info "data exported in #{bm}"
195
+ result
196
+ end
197
+
198
+ def export_attachments(record)
199
+ record.attributes.keys.select { |key| key =~ /^img/i }.each { |key|
200
+ file = record.attributes[key]
201
+ next if file.blank?
202
+ path = File.expand_path(file, DataGateway.resource_path)
203
+ #TODO: Si el fichero ya se exporto solo en este vuelta, no repetir!
204
+ next unless File.exist?(path)
205
+ modified = File.mtime(path) > DataGateway.last_time['export']
206
+ next unless modified
207
+ DataGateway.logger.info "exporting attach #{path}"
208
+ attach_data = File.open(path, 'rb') { |f| f.read }
209
+ write_file(File.join('attach', file), attach_data, 'b')
210
+ }
211
+ end
212
+
213
+ def write_file(file_name, data, options = '')
214
+ op = StringIO.new("", "w")
215
+ op.puts data
216
+ if (options == 'b')
217
+ export_folder.open(file_name, "w" + options){|f| f.puts op.string}
218
+ else
219
+ export_folder.open(file_name, "w" + options){|f| f.puts @encoder.pack(op.string)}
220
+ end
221
+ end
222
+ end
223
+ end
@@ -5,19 +5,20 @@ db_connection AppConfig.connections[:nexusdb]
5
5
  export :nexus_export do
6
6
  encode( :to => "UTF-8" , :from => "WINDOWS-1252")
7
7
  export_to :csv
8
- exporting :user, :conditions => 'CODCLI IS NOT NULL'
9
- exporting :category
8
+ exporting :user,
9
+ :select => 'USERID AS LOGIN, CODCLI AS CODE, EMAIL',
10
+ :conditions => 'CODCLI IS NOT NULL'
11
+ exporting :category
10
12
  exporting :family
11
13
 
12
14
  exporting :news,
13
- :select => 'DESCRIPTION AS TITLE, FROMDATE AS PUBLISHED_AT, TODATE AS EXPIRES_AT, PRIORITY, NEWSTEXT BODY'
14
- exporting :product, # :limit => 5,
15
+ :select => 'ID AS CODE, DESCRIPTION AS TITLE, FROMDATE AS PUBLISHED_AT, TODATE AS EXPIRES_AT, PRIORITY, NEWSTEXT BODY'
16
+ exporting :product,
15
17
  :select => 'CATEGORY_ID, FAMILY_ID, CODE, IMG_DETAIL, IMG_MINI, DETAIL, da.DESCART AS LABEL, PRCVENTA',
16
18
  :joins => 'as p left outer join ARTICULO as ar on CODE = ar.CODART left outer join DESCRIPA as da on CODE = da.CODART',
17
19
  :conditions => "da.codidioma = 'CAS'"
18
-
20
+
19
21
  exporting :tarifa_venta,
20
22
  :select => 'IDTARIFAV, CODART, FECMAX, PRECIO, TARIFA, UNIDADES',
21
23
  :joins => 'as tarf inner join mmproducts p on code = tarf.codart'
22
24
  end
23
-
@@ -7,10 +7,7 @@ import(:nexus_import) do
7
7
  importing :User do
8
8
  force_upcase
9
9
  identity_by :USERID
10
- columns :full_name => none
11
- #:userid => write_to(:USERID),
12
- #:email => :EMAIL,
13
-
10
+ columns :full_name => none
14
11
  end
15
12
 
16
13
  ##FIX: no puede ser LineItem pq hay varios documentos -> LineOrder
@@ -3,14 +3,17 @@ db_connection AppConfig.connections[:shopindb]
3
3
 
4
4
  export 'shopin_export' do
5
5
  exporting :user,
6
- :select => 'login as userid, email',
6
+ :select => "login as userid, email, ' 1' as codcli",
7
7
  :conditions => 'activated_at is not null'
8
8
 
9
9
  exporting :order,
10
10
  :select => 'number, reference, special_instructions, login as userid',
11
- :joins => 'as ord left outer join users as usr on ord.user_id = usr.id'
12
-
11
+ :joins => 'as ord left outer join users as usr on ord.user_id = usr.id',
12
+ :conditions => "(ord.updated_at > '#{last_time(:export)}')"
13
+
13
14
  exporting :line_items,
14
- :select => 'line.id as id, number as order_id, price, product_id, quantity',
15
- :joins => 'as line left outer join orders as ord on line.order_id = ord.id'
16
- end
15
+ :select => 'line.id as id, number as order_id, price, product_id, quantity',
16
+ :joins => 'as line left outer join orders as ord on line.order_id = ord.id',
17
+ :conditions => "(ord.updated_at > '#{last_time(:export)}')"
18
+
19
+ end
@@ -18,16 +18,17 @@ import 'shopin_import' do
18
18
  columns :DESCRIPTION => :name,
19
19
  :DBREV => none
20
20
  end
21
-
21
+
22
22
  importing :News do
23
+ identity_by :code
24
+ force_downcase
23
25
  target :Post
24
- force_downcase
25
26
  end
26
-
27
+
27
28
  importing :User do
28
- ignore!
29
+ identity_by :login
29
30
  end
30
-
31
+
31
32
  importing :Product do
32
33
  identity_by :code
33
34
  force_downcase
@@ -35,7 +36,7 @@ import 'shopin_import' do
35
36
  :DETAIL => :description,
36
37
  :PRCVENTA => :price
37
38
  end
38
-
39
+
39
40
  importing :TarifaVenta do
40
41
  target :PriceList
41
42
  identity_by :code
@@ -45,6 +46,6 @@ import 'shopin_import' do
45
46
  :FECMAX => :expires_at,
46
47
  :PRECIO => :price,
47
48
  :TARIFA => :group,
48
- :UNIDADES => :quantity
49
+ :UNIDADES => :quantity
49
50
  end
50
- end
51
+ end
@@ -0,0 +1,226 @@
1
+ module DataGateway
2
+ class ColumnMapper
3
+ attr :options
4
+ def initialize(table_mapper, options = {})
5
+ @table_mapper = table_mapper
6
+ @options = options
7
+ end
8
+
9
+ def apply_to(record, value)
10
+ DataGateway.logger.debug "apply_to(#{record.inspect}, #{value}) options #{@options.inspect}"
11
+ send(@options[:method], record, value)
12
+ end
13
+
14
+ # value mappers
15
+ def none(record, value)
16
+ # do nothing
17
+ end
18
+
19
+ def write_to(record, value)
20
+ record[@options[:write_to]] = value
21
+ end
22
+
23
+ def lookup(record, value)
24
+
25
+ result_status = {}
26
+ new_value = lookup_map(value, result_status)
27
+ if result_status[:found]
28
+ write_to(record, new_value)
29
+ else
30
+ @table_mapper.register_fixup(value, self)
31
+ end
32
+ end
33
+
34
+ private
35
+ def lookup_map(value, result_status)
36
+ DataGateway.logger.debug "lookup_map(#{value}, #{result_status}) options #{@options.inspect}"
37
+ lookup_model = DataGateway.model_for(@options[:table])
38
+ finder = "find_or_create_by_#{@options[:key]}"
39
+ lookup_rec = lookup_model.send(finder, value)
40
+ result_status[:found] = !(lookup_rec.nil?)
41
+ if result_status[:found]
42
+ lookup_rec.send(@options[:value]).to_s # valor mapeado
43
+ else
44
+ nil
45
+ end
46
+ end
47
+
48
+ def solve_fixup(record, value)
49
+ DataGateway.logger.debug "mapper #{@map_method} opt. #{@options.inspect}"
50
+ result_status = {}
51
+ DataGateway.logger.debug "lookup #{record.inspect} -> <#{value}>"
52
+ value = lookup_map(value, result_status)
53
+ if result_status[:found]
54
+ DataGateway.logger.debug "FOUND: #{value}"
55
+ write_to(record, value)
56
+ else
57
+ DataGateway.logger.debug "NOT FOUND!"
58
+ end
59
+ end
60
+ end
61
+
62
+ class TableMapper
63
+ attr :source
64
+ attr :identity
65
+
66
+ def initialize(job, table)
67
+ @job = job
68
+ @source = table
69
+ @target = table
70
+ @column_map = {}
71
+ @identity = [:id]
72
+ @ar_fixups = []
73
+ @force_case = false
74
+ clear_fixups
75
+ end
76
+
77
+ def ignore!
78
+ @target = nil
79
+ end
80
+
81
+ def ignore?
82
+ @target == nil
83
+ end
84
+
85
+ def target(table_name = nil)
86
+ @target = table_name if table_name
87
+ @target
88
+ end
89
+
90
+ def force_downcase
91
+ @force_case = :downcase
92
+ end
93
+
94
+ def force_upcase
95
+ @force_case = :upcase
96
+ end
97
+
98
+ def target_model
99
+ DataGateway.model_for(@target)
100
+ end
101
+
102
+ # DSL syntax sugar for declare column mappers
103
+ def columns(column_map)
104
+ column_map.each_pair do |key, value|
105
+ case value
106
+ when Symbol, String: column_map[key] = write_to(value.to_sym)
107
+ end
108
+ end
109
+ @column_map.merge! column_map
110
+
111
+ @column_map.each_pair do |key, value|
112
+ defaults = column_mapper_defaults(key)
113
+ @column_map[key].options.replace(defaults.merge!(@column_map[key].options))
114
+ #puts "map for #{key} => #{@column_map[key].options.inspect}"
115
+ end
116
+ @column_map
117
+ end
118
+
119
+ def none
120
+ create_column_mapper({:method => :none})
121
+ end
122
+
123
+ def write_to(column)
124
+ create_column_mapper({:method => :write_to, :write_to => column})
125
+ end
126
+
127
+ def lookup(options)
128
+ options[:method] = :lookup
129
+ create_column_mapper(options)
130
+ end
131
+
132
+ def identity_by(*cols)
133
+ @identity = cols
134
+ end
135
+
136
+ # utilities
137
+ def column_mapper_for(column_name)
138
+ @column_map[column_name] ||= create_column_mapper(column_mapper_defaults(column_name))
139
+ end
140
+
141
+ def column_mapper_defaults(column_name)
142
+ column_name = column_name.to_s.send(@force_case).to_sym if @force_case
143
+ case column_name.to_s
144
+ when /(.*)_id$/i: { :method => :lookup,
145
+ :write_to => column_name,
146
+ :table => $1.downcase.camelize.to_sym,
147
+ :key => @job.get_mapper($1.downcase.camelize.to_sym).identity,
148
+ :value => :id
149
+ }
150
+ else { :method => :write_to,
151
+ :write_to => column_name
152
+ }
153
+ end
154
+
155
+ end
156
+
157
+ def update_all(records)
158
+ DataGateway.logger.debug "importing #{records.size} recods into table #{source}"
159
+ klass = target_model
160
+ DataGateway.logger.debug "target class #{klass} (PK: #{klass.primary_key})"
161
+ records.each { |rec|
162
+ rec = map(rec)
163
+ update_rec(rec) unless current_fixups(rec)
164
+ }
165
+ end
166
+
167
+ def clear_fixups
168
+ @fixups = {}
169
+ end
170
+
171
+ def register_fixup(value, col_mapper)
172
+ @ar_fixups << [value, col_mapper]
173
+ end
174
+
175
+ def current_fixups(rec)
176
+ DataGateway.logger.debug "current fixups for: #{rec.inspect}"
177
+ DataGateway.logger.debug @ar_fixups.inspect
178
+ return false if @ar_fixups.empty?
179
+ @fixups[rec] ||= []
180
+ @fixups[rec].concat @ar_fixups
181
+ @ar_fixups = []
182
+ true
183
+ end
184
+
185
+ def solve_fixups
186
+ DataGateway.logger.debug "start solve_fixups #{@fixups.size}"
187
+ @fixups.each_pair { |record, data|
188
+ data.each{|item|
189
+ value = item[0]
190
+ col_mapper = item[1]
191
+ col_mapper.solve_fixup(record, value)
192
+ }
193
+ update_rec(record)
194
+ }
195
+ DataGateway.logger.debug "end solve_fixups"
196
+ ensure
197
+ clear_fixups
198
+ end
199
+
200
+ def update_rec(rec)
201
+ DataGateway.logger.debug "update rec class #{target_model} '->' #{rec.inspect}"
202
+ conditions = identity.map{|key| key.to_s}
203
+ values = conditions.map{|key| rec.delete(key.to_sym)}
204
+ finder = 'find_or_create_by_' + conditions.join('_')
205
+ DataGateway.logger.debug "finder: #{finder}(#{values.join(',')})"
206
+
207
+ ar = target_model.send(finder, *values)
208
+ DataGateway.logger.debug "get: #{ar.inspect}"
209
+ DataGateway.logger.debug "upd atrt: #{rec.inspect}"
210
+ ar.update_attributes(rec)
211
+ return ar[:id]
212
+ end
213
+
214
+ def map(ar)
215
+ new_ar = {}
216
+ ar.each_pair do |key, value|
217
+ column_mapper_for(key).apply_to(new_ar, value)
218
+ end
219
+ new_ar
220
+ end
221
+ private
222
+ def create_column_mapper( options)
223
+ ColumnMapper.new(self, options)
224
+ end
225
+ end
226
+ end
File without changes
File without changes
File without changes
File without changes