resque-reports 0.0.1

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