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 +3 -0
- data/.rubocop.yml +18 -0
- data/app/jobs/resque/reports/report_job.rb +48 -8
- data/lib/resque/reports/base_report.rb +151 -98
- data/lib/resque/reports/cache_file.rb +22 -10
- data/lib/resque/reports/csv_report.rb +54 -30
- data/lib/resque/reports/extensions/const.rb +11 -0
- data/lib/resque/reports/extensions/encodings.rb +11 -0
- data/lib/resque/reports/extensions/event_callbacks.rb +64 -0
- data/lib/resque/reports/extensions/event_templates.rb +22 -0
- data/lib/resque/reports/extensions/filename_gen.rb +29 -0
- data/lib/resque/reports/extensions/table_building.rb +117 -0
- data/lib/resque/reports/extensions.rb +37 -0
- data/lib/resque/reports/version.rb +2 -1
- data/lib/resque/reports.rb +6 -5
- data/lib/resque-reports.rb +1 -0
- data/resque-reports.gemspec +7 -5
- data/spec/resque/reports/base_report_spec.rb +196 -0
- data/spec/resque/reports/csv_report_spec.rb +89 -0
- data/spec/resque/reports/report_job_spec.rb +140 -0
- data/spec/spec_helper.rb +8 -73
- metadata +63 -13
- data/init.rb +0 -4
- data/lib/resque/reports/callbacks.rb +0 -50
- data/lib/resque/reports/encodings.rb +0 -13
data/.rspec
ADDED
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
|
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
|
-
|
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 =
|
13
|
-
|
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
|
-
|
16
|
-
|
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
|
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
|
61
|
+
include Extensions
|
9
62
|
|
10
63
|
class << self
|
11
|
-
protected
|
12
64
|
|
13
|
-
attr_reader :
|
14
|
-
:directory,
|
15
|
-
:create_block
|
65
|
+
attr_reader :job_queue
|
16
66
|
|
17
|
-
|
18
|
-
:encoding,
|
19
|
-
:source_method,
|
20
|
-
:table_block,
|
21
|
-
:header_collecting
|
22
|
-
|
23
|
-
alias_method :source, :source_method
|
67
|
+
protected
|
24
68
|
|
25
|
-
|
26
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
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
|
50
|
-
|
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
|
-
|
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
|
-
|
140
|
+
# TODO: Check consistance, fail if user initialized wrong object
|
141
|
+
set_instance(self)
|
62
142
|
|
63
|
-
|
64
|
-
|
143
|
+
if create_block
|
144
|
+
define_singleton_method(:create_dispatch, create_block)
|
145
|
+
create_dispatch(*args)
|
146
|
+
end
|
65
147
|
|
66
|
-
@
|
148
|
+
@args = args
|
67
149
|
|
150
|
+
init_cache_file
|
68
151
|
init_table
|
69
152
|
end
|
70
153
|
|
71
|
-
|
72
|
-
|
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
|
-
|
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
|
-
|
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)
|
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
|
-
|
91
|
-
|
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
|
-
|
141
|
-
|
142
|
-
|
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
|
-
|
147
|
-
|
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 =
|
7
|
-
DEFAULT_CODING =
|
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
|
-
|
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
|
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)
|
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
|
-
|
66
|
-
|
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
|