resque-reports 0.0.2 → 0.3.0

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.
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