resque-reports 0.0.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format progress
3
+ --order random
data/.rubocop.yml ADDED
@@ -0,0 +1,18 @@
1
+ # This is the configuration used to check the rubocop source code.
2
+ # inherit_from: config/default.yml
3
+
4
+ MethodLength:
5
+ Max: 20
6
+ LineLength:
7
+ Max: 80
8
+
9
+ AsciiComments:
10
+ Enabled: false
11
+ SpaceInsideHashLiteralBraces:
12
+ EnforcedStyleIsWithSpaces: false
13
+ Documentation:
14
+ Enabled: false
15
+ TrivialAccessors:
16
+ Enabled: false
17
+ EmptyLiteral:
18
+ Enabled: false
@@ -1,21 +1,61 @@
1
1
  # coding: utf-8
2
+ require 'json'
3
+ require 'active_support'
4
+
2
5
  module Resque
3
6
  module Reports
4
- # ReportJob accepts report_type and current report arguments to build it in background
7
+ # ReportJob accepts report_type, its arguments in json
8
+ # and building report in background
9
+ # @example:
10
+ #
11
+ # ReportJob.enqueue('Resque::Reports::MyReport', [1, 2].to_json)
12
+ #
5
13
  class ReportJob
6
14
  include Resque::Integration
7
15
 
8
- queue :reports
9
- unique { |report_type, args_json| [report_type, args_json] }
16
+ unique
10
17
 
18
+ # resque-integration main job method
19
+ # @param [String] report_type - name of BaseReport successor
20
+ # to build report for
21
+ # @param [String(JSON)] args_json - json array of report arguments
11
22
  def self.execute(report_type, args_json)
12
- report_class = constant(report_type) # Get report class from string (through ActiveSupport)
13
- raise "Resque::Reports::ReportJob can work only with successors of Resque::Reports::BaseReport, but got #{report_class}" unless report_class.ancestors.include? BaseReport
23
+ report_class = report_type.constantize # избавиться от ActiveSupport
24
+
25
+ unless report_class < BaseReport
26
+ fail "Supports only successors of BaseReport, but got #{report_class}"
27
+ end
28
+
29
+ fail 'Report queue is not specified' unless report_class.job_queue
30
+ queue report_class.job_queue
31
+
32
+ args = JSON.parse(args_json)
33
+ force = args.pop
34
+
35
+ init_report(report_class, args)
36
+ .build(force)
37
+ end
38
+
39
+ private
40
+
41
+ # Initializes report of given class with given arguments
42
+ def self.init_report(report_class, args_array)
43
+ report = report_class.new(*args_array)
44
+
45
+ report_class.on_progress do |progress, total|
46
+ unless total.zero?
47
+ at(progress, total, report.progress_message(progress, total))
48
+ end
49
+ end
14
50
 
15
- args = Json.parse(args_json)
16
- report = report_class.new *args
51
+ report_class.on_error do |error|
52
+ meta = get_meta(@meta_id)
53
+ meta['payload'] ||= {'error_messages' => []}
54
+ meta['payload']['error_messages'] << report.error_message(error)
55
+ meta.save
56
+ end
17
57
 
18
- report.build
58
+ report
19
59
  end
20
60
  end # class ReportJob
21
61
  end # module Reports
@@ -1,150 +1,203 @@
1
1
  # coding: utf-8
2
+ # Resque namespace
2
3
  module Resque
4
+ # Resque::Reports namespace
3
5
  module Reports
6
+ # Class describes base report class for inheritance.
7
+ # BaseReport successor must implement "write(io, force)" method
8
+ # and may specify file extension with "extension" method call
9
+ # example:
10
+ #
11
+ # class CustomTypeReport < Resque::Reports::BaseReport
12
+ # extension :type # specify that report file must ends
13
+ # # with '.type', e.g. 'abc.type'
14
+ #
15
+ # # Method specifies how to output report data
16
+ # def write(io, force)
17
+ # io << 'Hello World!'
18
+ # end
19
+ # end
20
+ #
21
+ # BaseReport provides followed DSL, example:
22
+ #
23
+ # class CustomReport < CustomTypeReport
24
+ # queue :custom_reports # Resque queue name
25
+ # source :select_data # method called to retrieve report data
26
+ # encoding UTF8 # file encoding
27
+ #
28
+ # # Specify in which directory to keep this type files
29
+ # directory File.join(Dir.tmpdir, 'resque-reports')
30
+ #
31
+ # # Describe table using 'column' method
32
+ # table do |element|
33
+ # column 'Column 1 Header', :decorate_one
34
+ # column 'Column 2 Header', decorate_two(element[1])
35
+ # column 'Column 3 Header', 'Column 3 Cell'
36
+ # end
37
+ #
38
+ # # Class initialize
39
+ # # NOTE: must be used instead of define 'initialize' method
40
+ # create do |param|
41
+ # @main_param = param
42
+ # end
43
+ #
44
+ # # decorate method, called by symbol-name
45
+ # def decorate_one(element)
46
+ # "decorate_one: #{element[0]}"
47
+ # end
48
+ #
49
+ # # decorate method, called directly when filling cell
50
+ # def decorate_two(text)
51
+ # "decorate_two: #{text}"
52
+ # end
53
+ #
54
+ # # method returns report data Enumerable
55
+ # def select_data
56
+ # [[0, 'text0'], [1, 'text1']]
57
+ # end
58
+ # end
4
59
  class BaseReport
5
- # TODO: Hook initialize of successor to collect init params into @args array
6
- # include ActiveSupport
7
60
  extend Forwardable
8
- include Encodings # include encoding constants CP1251, UTF8...
61
+ include Extensions
9
62
 
10
63
  class << self
11
- protected
12
64
 
13
- attr_reader :row_object,
14
- :directory,
15
- :create_block
65
+ attr_reader :job_queue
16
66
 
17
- attr_accessor :extension,
18
- :encoding,
19
- :source_method,
20
- :table_block,
21
- :header_collecting
22
-
23
- alias_method :source, :source_method
67
+ protected
24
68
 
25
- def table(&block)
26
- @table_block = block
69
+ attr_reader :create_block
70
+ attr_writer :job_queue
71
+ attr_accessor :file_extension,
72
+ :file_encoding,
73
+ :file_directory
74
+
75
+ alias_method :super_extension, :file_extension
76
+ alias_method :extension, :file_extension=
77
+ alias_method :set_extension, :file_extension=
78
+ alias_method :encoding, :file_encoding=
79
+ alias_method :directory, :file_directory=
80
+ alias_method :queue, :job_queue=
81
+
82
+ def set_instance(obj)
83
+ @instance = obj
27
84
  end
28
85
 
29
86
  def create(&block)
30
87
  @create_block = block
31
88
  end
32
89
 
33
- def build_table_row(row_object)
34
- header_collecting = false
35
-
36
- @row_object = row_object # for instance decorate methods calls
37
- row = @table_block.call(row_object)
38
-
39
- finish_row
40
-
41
- row
90
+ # override for Extenstions::TableBuilding, to use custom encoding
91
+ def encoded_string(obj)
92
+ obj.to_s.encode(file_encoding,
93
+ invalid: :replace,
94
+ undef: :replace)
42
95
  end
43
96
 
44
- def build_table_header
45
- header_collecting = true
46
- @table_block.call(nil)
97
+ #--
98
+ # Hooks #
99
+ #++
100
+
101
+ def method_missing(method_name, *args, &block)
102
+ if @instance.respond_to?(method_name)
103
+ @instance.send(method_name, *args, &block)
104
+ else
105
+ super
106
+ end
47
107
  end
48
108
 
49
- def get_data
50
- send(@source_method)
109
+ def respond_to?(method, include_private = false)
110
+ super || @instance.respond_to?(method, include_private)
51
111
  end
52
112
  end # class methods
53
113
 
54
- # Constants
114
+ #--
115
+ # Constants #
116
+ #++
55
117
 
56
118
  DEFAULT_EXTENSION = 'txt'
57
119
 
120
+ #--
121
+ # Delegators
122
+ #++
123
+
124
+ def_delegators Const::TO_EIGENCLASS,
125
+ :file_directory,
126
+ :file_extension,
127
+ :file_encoding,
128
+ :create_block,
129
+ :set_instance,
130
+ :set_extension
131
+
132
+ def_delegators :@cache_file, :filename, :exists?, :ready?
133
+ def_delegator Const::TO_SUPER, :super_extension
134
+
135
+ #--
58
136
  # Public instance methods
137
+ #++
59
138
 
60
139
  def initialize(*args)
61
- create_block.call(*args) if create_block
140
+ # TODO: Check consistance, fail if user initialized wrong object
141
+ set_instance(self)
62
142
 
63
- @args = args
64
- extension ||= DEFAULT_EXTENSION
143
+ if create_block
144
+ define_singleton_method(:create_dispatch, create_block)
145
+ create_dispatch(*args)
146
+ end
65
147
 
66
- @cache_file = CacheFile.new(directory, generate_filename, coding: encoding)
148
+ @args = args
67
149
 
150
+ init_cache_file
68
151
  init_table
69
152
  end
70
153
 
71
- def build
72
- @cache_file.open { |file| write file }
154
+ # Builds report synchronously
155
+ def build(force = false)
156
+ init_table if force
157
+
158
+ @cache_file.open(force) { |file| write(file, force) }
73
159
  end
74
160
 
75
- def bg_build
161
+ # Builds report in background, returns job_id, to watch progress
162
+ def bg_build(force = false)
76
163
  report_class = self.class.to_s
77
- args_json = @args.to_json
164
+
165
+ args_json = [*@args, force].to_json
78
166
 
79
167
  # Check report if it already in progress and tring return its job_id...
80
168
  job_id = ReportJob.enqueued?(report_class, args_json).try(:meta_id)
81
-
82
169
  # ...and start new job otherwise
83
- ReportJob.enqueue(report_class, args_json) unless job_id
170
+ job_id || ReportJob.enqueue(report_class, args_json).try(:meta_id)
84
171
  end
85
172
 
86
- def_delegators :@cache_file, :filename, :exists?
87
-
88
173
  protected
89
174
 
90
- # You must use ancestor methods to work with report data:
91
- # 1) get_data => returns Enumerable of report source objects
92
- # 2) build_table_header => returns Array of report column names
93
- # 3) build_table_row(object) => returns Array of report cell values (same order as header)
94
- def write(io)
95
- raise NotImplementedError, "write must be implemented in successor"
96
- end
97
-
98
- def column(name, value)
99
- add_column_header(name) || add_column_cell(value)
100
- end
101
-
102
- private
103
-
104
- def_delegators 'self.class',
105
- :directory,
106
- :extension,
107
- :encoding,
108
- :get_data,
109
- :build_table_header,
110
- :build_table_row,
111
- :header_collecting,
112
- :row_object,
113
- :create_block
114
-
115
- # Fill report table #
116
-
117
- def init_table
118
- @table_header = []
119
- @table_row = []
120
- end
121
-
122
- def add_column_header(column_name)
123
- @table_header << column_name if header_collecting
124
- end
125
-
126
- def add_column_cell(column_value)
127
- return if header_collecting
128
- column_value = send(column_value, row_object) if column_value.is_a? Symbol
129
- @table_row << encoded_string(value)
130
- end
131
-
132
- def encoded_string(obj)
133
- obj.to_s.encode(encoding, :invalid => :replace, :undef => :replace)
134
- end
135
-
136
- def finish_row
137
- @table_row = []
138
- end
175
+ def init_cache_file
176
+ set_extension super_extension || DEFAULT_EXTENSION
139
177
 
140
- # Generate filename #
141
-
142
- def generate_filename
143
- "#{ self.class }-#{ hash_args }.#{ extension }"
178
+ @cache_file = CacheFile.new(file_directory,
179
+ generate_filename(@args, file_extension),
180
+ coding: file_encoding)
144
181
  end
145
182
 
146
- def hash_args
147
- Digest::SHA1.hexdigest(@args.to_json)
183
+ # Method specifies how to output report data
184
+ # @param [IO] io stream for output
185
+ # @param [true, false] force write to output or skip due its existance
186
+ def write(io, force)
187
+ # You must use ancestor methods to work with report data:
188
+ # 1) data_size => returns source data size (calls #count on data
189
+ # retrieved from 'source')
190
+ # 2) data_each => yields given block for each source data element
191
+ # 3) build_table_header => returns Array of report column names
192
+ # 4) build_table_row(object) => returns Array of report cell
193
+ # values (same order as header)
194
+ # 5) progress_message(progress,
195
+ # total) => call to iterate job progress
196
+ # 6) error_message(error) => call to handle error in job
197
+ #
198
+ # HINT: You may override data_size and data_each, to retrieve them
199
+ # effectively
200
+ fail NotImplementedError
148
201
  end
149
202
  end # class BaseReport
150
203
  end # module Report
@@ -1,15 +1,19 @@
1
1
  # coding: utf-8
2
2
  module Resque
3
3
  module Reports
4
+ # Class describes how to storage and access cache file
5
+ # NOTE: Every time any cache file is opening,
6
+ # cache is cleared from old files.
4
7
  class CacheFile
8
+ include Extensions::Encodings
5
9
 
6
- DEFAULT_EXPIRE_TIME = 86400
7
- DEFAULT_CODING = 'utf-8'
10
+ DEFAULT_EXPIRE_TIME = 86_400
11
+ DEFAULT_CODING = UTF8
8
12
 
9
- # TODO: Description!
10
13
  def initialize(dir, filename, options = {})
11
14
  @dir = dir
12
15
  @filename = File.join(dir, filename)
16
+ @ext = File.extname(filename)
13
17
 
14
18
  # options
15
19
  @coding = options[:coding] || DEFAULT_CODING
@@ -22,15 +26,17 @@ module Resque
22
26
  alias_method :ready?, :exists?
23
27
 
24
28
  def filename
25
- raise "File doesn't exists, check for its existance before" unless exists?
29
+ fail 'File doesn\'t exists, check exists? before' unless exists?
26
30
  @filename
27
31
  end
28
32
 
29
- def open
33
+ def open(force = false)
30
34
  prepare_cache_dir
31
35
 
36
+ force ? FileUtils.rm_f(@filename) : return if File.exists?(@filename)
37
+
32
38
  remove_unfinished_on_error do
33
- File.open(@filename, @coding) do |file|
39
+ File.open(@filename, "w:#{@coding}") do |file|
34
40
  yield file
35
41
  end
36
42
  end
@@ -45,7 +51,8 @@ module Resque
45
51
  end
46
52
 
47
53
  def clear_expired_files
48
- # TODO: avoid races when worker building his report longer than @expiration_time
54
+ # TODO: avoid races when worker building
55
+ # his report longer than @expiration_time
49
56
  files_to_delete = cache_files_array.select { |fname| expired?(fname) }
50
57
 
51
58
  FileUtils.rm_f files_to_delete
@@ -56,14 +63,19 @@ module Resque
56
63
  end
57
64
 
58
65
  def cache_files_array
59
- Dir.new(@dir).map { |fname| File.join(@dir.path, fname) }
66
+ Dir.new(@dir)
67
+ .map { |fname| File.join(@dir, fname) if File.extname(fname) == @ext }
68
+ .compact
60
69
  end
61
70
 
62
71
  def remove_unfinished_on_error
63
72
  yield
64
73
  rescue => error
65
- FileUtils.rm_f @filename # remove everything that was written due to it inconsistance
66
- raise error # don't suppress any errors here
74
+ # remove everything that was written due to it inconsistance
75
+ FileUtils.rm_f @filename
76
+
77
+ # don't suppress any errors here
78
+ raise error
67
79
  end
68
80
  end
69
81
  end