resque-reports 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+ source 'http://apress:montalcino@gems.dev.apress.ru'
3
+
4
+ # Specify your gem's dependencies in resque-reports.gemspec
5
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Dolganov Sergey
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,13 @@
1
+ Resque::Reports
2
+ ==============
3
+
4
+ Introduction goes here.
5
+
6
+
7
+ Example
8
+ =======
9
+
10
+ Example goes here.
11
+
12
+
13
+ Copyright (c) 2013 Dolganov Sergey, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,22 @@
1
+ # coding: utf-8
2
+ module Resque
3
+ module Reports
4
+ # ReportJob accepts report_type and current report arguments to build it in background
5
+ class ReportJob
6
+ include Resque::Integration
7
+
8
+ queue :reports
9
+ unique { |report_type, args_json| [report_type, args_json] }
10
+
11
+ 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
14
+
15
+ args = Json.parse(args_json)
16
+ report = report_class.new *args
17
+
18
+ report.build
19
+ end
20
+ end # class ReportJob
21
+ end # module Reports
22
+ end # module Resque
data/init.rb ADDED
@@ -0,0 +1,4 @@
1
+ # coding: utf-8
2
+ Core.init_plugin do
3
+ ActiveSupport::Dependencies.autoload_paths << "#{File.dirname(__FILE__)}/app/jobs"
4
+ end
@@ -0,0 +1,151 @@
1
+ # coding: utf-8
2
+ module Resque
3
+ module Reports
4
+ class BaseReport
5
+ # TODO: Hook initialize of successor to collect init params into @args array
6
+ # include ActiveSupport
7
+ extend Forwardable
8
+ include Encodings # include encoding constants CP1251, UTF8...
9
+
10
+ class << self
11
+ protected
12
+
13
+ attr_reader :row_object,
14
+ :directory,
15
+ :create_block
16
+
17
+ attr_accessor :extension,
18
+ :encoding,
19
+ :source_method,
20
+ :table_block,
21
+ :header_collecting
22
+
23
+ alias_method :source_method, :source
24
+
25
+ def table(&block)
26
+ @table_block = block
27
+ end
28
+
29
+ def create(&block)
30
+ @create_block = block
31
+ end
32
+
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
42
+ end
43
+
44
+ def build_table_header
45
+ header_collecting = true
46
+ @table_block.call(nil)
47
+ end
48
+
49
+ def get_data
50
+ send(@source_method)
51
+ end
52
+ end # class methods
53
+
54
+ # Constants
55
+
56
+ DEFAULT_EXTENSION = 'txt'
57
+
58
+ # Public instance methods
59
+
60
+ def initialize(*args)
61
+ create_block.call(*args) if create_block
62
+
63
+ @args = args
64
+ extension ||= DEFAULT_EXTENSION
65
+
66
+ @cache_file = CacheFile.new(directory, generate_filename, coding: encoding)
67
+
68
+ init_table
69
+ end
70
+
71
+ def build
72
+ @cache_file.open { |file| write file }
73
+ end
74
+
75
+ def bg_build
76
+ report_class = self.class.to_s
77
+ args_json = @args.to_json
78
+
79
+ # Check report if it already in progress and tring return its job_id...
80
+ job_id = ReportJob.enqueued?(report_class, args_json).try(:meta_id)
81
+
82
+ # ...and start new job otherwise
83
+ ReportJob.enqueue(report_class, args_json) unless job_id
84
+ end
85
+
86
+ def_delegators :@cache_file, :filename, :exists?
87
+
88
+ protected
89
+
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
139
+
140
+ # Generate filename #
141
+
142
+ def generate_filename
143
+ "#{ self.class }-#{ hash_args }.#{ extension }"
144
+ end
145
+
146
+ def hash_args
147
+ Digest::SHA1.hexdigest(@args.to_json)
148
+ end
149
+ end # class BaseReport
150
+ end # module Report
151
+ end # module Resque
@@ -0,0 +1,70 @@
1
+ # coding: utf-8
2
+ module Resque
3
+ module Reports
4
+ class CacheFile
5
+
6
+ DEFAULT_EXPIRE_TIME = 1.day
7
+ DEFAULT_CODING = 'utf-8'
8
+
9
+ # TODO: Description!
10
+ def initialize(dir, filename, options = {})
11
+ @dir = dir
12
+ @filename = File.join(dir, filename)
13
+
14
+ # options
15
+ @coding = options[:coding] || DEFAULT_CODING
16
+ @expiration_time = options[:expire_in] || DEFAULT_EXPIRE_TIME
17
+ end
18
+
19
+ def exists?
20
+ File.exists?(@filename)
21
+ end
22
+ alias_method :exists?, :ready?
23
+
24
+ def filename
25
+ raise "File doesn't exists, check for its existance before" unless exists?
26
+ @filename
27
+ end
28
+
29
+ def open
30
+ prepare_cache_dir
31
+
32
+ remove_unfinished_on_error do
33
+ File.open(@filename, @coding) do |file|
34
+ yield file
35
+ end
36
+ end
37
+ end
38
+
39
+ protected
40
+
41
+ def prepare_cache_dir
42
+ FileUtils.mkdir_p @dir # create folder if not exists
43
+
44
+ clear_expired_files
45
+ end
46
+
47
+ def clear_expired_files
48
+ # TODO: avoid races when worker building his report longer than @expiration_time
49
+ files_to_delete = cache_files_array.select { |fname| expired?(fname) }
50
+
51
+ FileUtils.rm_f files_to_delete
52
+ end
53
+
54
+ def expired?(fname)
55
+ File.file?(fname) && File.mtime(fname) + @expiration_time < Time.now
56
+ end
57
+
58
+ def cache_files_array
59
+ Dir.new(@dir).map { |fname| File.join(@dir.path, fname) }
60
+ end
61
+
62
+ def remove_unfinished_on_error
63
+ yield
64
+ 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
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,50 @@
1
+ # coding: utf-8
2
+ module Resque
3
+ module Reports
4
+ module Callbacks
5
+
6
+ # TODO: сделать гибкой логику колбеков и хендлеров
7
+ module ClassMethods
8
+ protected
9
+
10
+ PROGRESS_INTERVAL = 10
11
+
12
+ # Callbacks
13
+
14
+ # rubocop:disable TrivialAccessors
15
+
16
+ # Set callback for watching progress of export
17
+ # @yield [progress] block to be executed on progress
18
+ # @yieldparam progress [Integer] current progress
19
+ # @yieldparam total [Integer] data length
20
+ def on_progress(&block)
21
+ @progress_callback = block
22
+ end
23
+
24
+ # Set callback on error
25
+ # @yield [error] block to be executed when error occurred
26
+ # @yieldparam [Exception] error
27
+ def on_error(&block)
28
+ @error_callback = block
29
+ end
30
+
31
+ # rubocop:enable TrivialAccessors
32
+
33
+ # Handlers
34
+ def handle_progress(progress, force = false)
35
+ if @progress_callback && (force || progress % self.class::PROGRESS_INTERVAL == 0)
36
+ @progress_callback.call progress, @data.size
37
+ end
38
+ end
39
+
40
+ def handle_error
41
+ @error_callback ? @error_callback.call($ERROR_INFO) : raise
42
+ end
43
+ end
44
+ end
45
+
46
+ def self.included(base)
47
+ base.extend ClassMethods
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,60 @@
1
+ # coding: utf-8
2
+ module Resque
3
+ module Reports
4
+ class CsvReport < BaseReport
5
+ extend Forwardable
6
+ include Callbacks # include on_progress, on_error callbacks, and handle_progress, handle_errors handlers
7
+
8
+ class << self
9
+ attr_accessor :csv_options
10
+ end
11
+
12
+ DEFAULT_CSV_OPTIONS = { col_sep: ';', row_sep: "\r\n" }
13
+
14
+ extension :csv
15
+
16
+ def_delegator 'self.class', :csv_options
17
+
18
+ def initialize(*args)
19
+ csv_options = DEFAULT_CSV_OPTIONS.merge(csv_options)
20
+ super(*args)
21
+ end
22
+
23
+ # Callbacks
24
+ on_progress { |progress, total| at(progress, total, progress_message(progress, total)) }
25
+ on_error { |error| raise error }
26
+
27
+ # You must use ancestor methods to work with data:
28
+ # 1) get_data => returns Enumerable of source objects
29
+ # 2) build_table_header => returns Array of column names
30
+ # 3) build_table_row(object) => returns Array of cell values (same order as header)
31
+ def write(io)
32
+ progress = 0
33
+
34
+ CSV(io, csv_options) do |csv|
35
+ data_collection = get_data
36
+
37
+ if data_collection.size > 0
38
+ write_line csv, build_table_header
39
+
40
+ data_collection.each do |data_element|
41
+ begin
42
+ write_line csv, build_table_row(data_element)
43
+ rescue
44
+ handle_error
45
+ end
46
+
47
+ handle_progress(progress += 1)
48
+ end
49
+
50
+ handle_progress(progress, true)
51
+ end
52
+ end
53
+ end
54
+
55
+ def write_line(csv, row_cells)
56
+ csv << row_cells
57
+ end
58
+ end # class CsvReport
59
+ end # module Report
60
+ end # module Resque
@@ -0,0 +1,13 @@
1
+ # coding: utf-8
2
+ module Resque
3
+ module Reports
4
+ module Encodings
5
+ def self.included(base)
6
+ base.class_eval do
7
+ CP1251 = 'w:windows1251'
8
+ UTF8 = 'utf-8'
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module Resque
2
+ module Reports
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ # coding: utf-8
2
+ require 'forwardable'
3
+ require 'facets/kernel/constant'
4
+
5
+ require 'resque-integration'
6
+
7
+ require 'resque/reports/version'
8
+ require 'resque/reports/cache_file'
9
+ require 'resque/reports/callbacks'
10
+ require 'resque/reports/encodings'
11
+ require 'resque/reports/base_report'
12
+ require 'resque/reports/csv_report'
13
+
14
+ module Resque
15
+ module Reports
16
+
17
+ end
18
+ end
@@ -0,0 +1,2 @@
1
+ # coding: utf-8
2
+ require 'resque/reports'
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'resque/reports/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'resque-reports'
8
+ gem.version = Resque::Reports::VERSION
9
+ gem.authors = ['Sergey D.']
10
+ gem.email = ['sclinede@gmail.com']
11
+ gem.description = 'Make your custom reports to CSV with resque by simple DSL'
12
+ gem.summary = 'resque-reports 0.0.1'
13
+ gem.homepage = 'https://github.com/sclinede/resque-reports'
14
+ gem.license = "MIT"
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ['lib']
20
+
21
+ gem.add_dependency 'resque-integration', '~> 0.2.9'
22
+ gem.add_dependency 'facets', '>= 2.9.3'
23
+
24
+ gem.add_development_dependency "bundler", "~> 1.3"
25
+ gem.add_development_dependency "rake"
26
+ gem.add_development_dependency 'rspec'
27
+ end
@@ -0,0 +1,75 @@
1
+ # -*- encoding : utf-8 -*-
2
+ # - Sort through your spec_helper file. Place as much environment loading
3
+ # code that you don't normally modify during development in the
4
+ # Spork.prefork block.
5
+ # - Place the rest under Spork.each_run block
6
+ # - Any code that is left outside of the blocks will be ran during preforking
7
+ # and during each_run!
8
+ # - These instructions should self-destruct in 10 seconds. If they don't,
9
+ # feel free to delete them.
10
+ require 'spork'
11
+
12
+ Spork.prefork do
13
+ require 'active_support'
14
+ # Loading more in this block will cause your tests to run faster. However,
15
+ # if you change any configuration or code from libraries loaded here, you'll
16
+ # need to restart spork for it take effect.
17
+ # This file is copied to spec/ when you run 'rails generate rspec:install'
18
+ ENV['RAILS_ENV'] ||= 'test'
19
+
20
+ FileUtils.cp(File.expand_path('../../../../../config/sphinx.yml', __FILE__), File.expand_path('../dummy/config/sphinx.yml', __FILE__))
21
+
22
+ require File.expand_path('../dummy/config/application', __FILE__)
23
+ require "#{Rails.application.paths.vendor.plugins.first}/core_domains/lib/core/domains/testing/test_helper"
24
+ require File.expand_path('../dummy/config/environment', __FILE__)
25
+ require 'rspec/rails'
26
+ require 'rspec/autorun'
27
+ require 'factory_girl'
28
+ require 'shoulda-matchers'
29
+ require 'capybara/rails'
30
+ require 'capybara/rspec'
31
+
32
+ # Requires supporting ruby files with custom matchers and macros, etc,
33
+ # in spec/support/ and its subdirectories.
34
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
35
+ Dir["#{Rails.application.paths.vendor.plugins.first}/core/spec/support/**/*.rb"].each { |f| require f }
36
+ Dir["#{File.dirname(__FILE__)}/factories/**/*.rb"].each { |f| require f }
37
+
38
+ use_plugin_support
39
+ use_plugin_factories
40
+
41
+ RSpec.configure do |config|
42
+ # ## Mock Framework
43
+ #
44
+ # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
45
+ #
46
+ # config.mock_with :mocha
47
+ # config.mock_with :flexmock
48
+ # config.mock_with :rr
49
+
50
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
51
+ config.fixture_path = "#{File.dirname(__FILE__)}/fixtures"
52
+
53
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
54
+ # examples within a transaction, remove the following line or assign false
55
+ # instead of true.
56
+ config.use_transactional_fixtures = true
57
+
58
+ # If true, the base class of anonymous controllers will be inferred
59
+ # automatically. This will be the default behavior in future versions of
60
+ # rspec-rails.
61
+ config.infer_base_class_for_anonymous_controllers = false
62
+
63
+ if ENV['CI'].present?
64
+ config.color_enabled = true
65
+ config.tty = true
66
+ config.formatter = :documentation
67
+ end
68
+
69
+ config.backtrace_clean_patterns = []
70
+ end
71
+ end
72
+
73
+ Spork.each_run do
74
+ # This code will be run each time you run your specs.
75
+ end
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque-reports
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Sergey D.
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-09-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: resque-integration
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.2.9
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.2.9
30
+ - !ruby/object:Gem::Dependency
31
+ name: facets
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 2.9.3
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 2.9.3
46
+ - !ruby/object:Gem::Dependency
47
+ name: bundler
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '1.3'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rake
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: Make your custom reports to CSV with resque by simple DSL
95
+ email:
96
+ - sclinede@gmail.com
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - .gitignore
102
+ - Gemfile
103
+ - MIT-LICENSE
104
+ - README
105
+ - Rakefile
106
+ - app/jobs/resque/reports/report_job.rb
107
+ - init.rb
108
+ - lib/resque-reports.rb
109
+ - lib/resque/reports.rb
110
+ - lib/resque/reports/base_report.rb
111
+ - lib/resque/reports/cache_file.rb
112
+ - lib/resque/reports/callbacks.rb
113
+ - lib/resque/reports/csv_report.rb
114
+ - lib/resque/reports/encodings.rb
115
+ - lib/resque/reports/version.rb
116
+ - resque-reports.gemspec
117
+ - spec/spec_helper.rb
118
+ homepage: https://github.com/sclinede/resque-reports
119
+ licenses:
120
+ - MIT
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ! '>='
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ none: false
133
+ requirements:
134
+ - - ! '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubyforge_project:
139
+ rubygems_version: 1.8.24
140
+ signing_key:
141
+ specification_version: 3
142
+ summary: resque-reports 0.0.1
143
+ test_files:
144
+ - spec/spec_helper.rb
145
+ has_rdoc: