adhoc_script 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Ryan Funduk 2013
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9
+ of the Software, and to permit persons to whom the Software is furnished to do
10
+ so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,84 @@
1
+ # AdhocScript
2
+
3
+ So many times, mostly in Rails app, I find myself making a file
4
+ like `bin/adhoc/regen_user_avatar_jan_2013.rb` that goes like this:
5
+
6
+ ~~~ ruby
7
+ require File.expand_path( '../config/environment.rb', __FILE__ )
8
+
9
+ User.where( 'avatar_upload IS NOT NULL' ).find_each do |user|
10
+ user.regenerate_avatar!
11
+ end
12
+ ~~~
13
+
14
+ ...or something along those lines. And then I want to know, well, how
15
+ long will this take? How far through all these users am I?
16
+
17
+ What I _don't_ want to do is implement this progress status stuff everytime
18
+ myself. So now I can just use `AdhocScript` :)
19
+
20
+
21
+ ## Installation
22
+
23
+ Most likely you update your Gemfile and `bundle install`.
24
+
25
+ ~~~ ruby
26
+ gem 'adhoc_script', require: false
27
+ ~~~
28
+
29
+
30
+ ## Usage
31
+
32
+ The script above becomes:
33
+
34
+ ~~~ ruby
35
+ require File.expand_path( '../config/environment.rb', __FILE__ )
36
+ require 'adhoc_script'
37
+
38
+ scope = User.where( 'avatar_upload IS NOT NULL' )
39
+ AdhocScript.new( 'Regenerating avatars', scope ).run do |user|
40
+ user.regenerate_avatar!
41
+ end
42
+ ~~~
43
+
44
+ ...almost the same, really. But the output looks like this:
45
+
46
+ ~~~
47
+ - Regenerating avatars: 50% (Remaining: 26 minutes)
48
+ ~~~
49
+
50
+ ...and then a little while later:
51
+
52
+ ~~~
53
+ / Regenerating avatars: 75% (Remaining: 13 minutes)
54
+ ~~~
55
+
56
+ You can also use it with just about any object that responds to `#count`:
57
+
58
+ ~~~ ruby
59
+ #!/usr/bin/env ruby
60
+ # A completely rediculous example!
61
+
62
+ require 'adhoc_script'
63
+
64
+ $total = 0
65
+ def add( n )
66
+ $total += n
67
+ end
68
+
69
+ AdhocScript.new( 'Count to 10000', (1..100000), :each ).run(&method(:add))
70
+
71
+ puts "Total: #{$total}"
72
+ ~~~
73
+
74
+
75
+
76
+ ## Contributing
77
+
78
+ Maybe we could use a few other formatters? A progress bar, etc?
79
+
80
+ 1. Clone git://github.com/rfunduk/adhoc_script.git
81
+ 2. Create local branch.
82
+ 3. Make changes.
83
+ 4. `rake test` and return to 3 until passing.
84
+ 5. Commit, push and open a pull request.
@@ -0,0 +1,5 @@
1
+ task :test do
2
+ ruby "test/*_test.rb"
3
+ end
4
+
5
+ task :default => [:test]
@@ -0,0 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/adhoc_script', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Ryan Funduk"]
6
+ gem.email = ["ryan.funduk@gmail.com"]
7
+ gem.description = %q{A simple wrapper class for running adhoc scripts on sets of data.}
8
+ gem.summary = %q{}
9
+ gem.homepage = "http://github.com/rfunduk/adhoc_script"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- test/*`.split("\n")
14
+ gem.name = "adhoc_script"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = '0.0.1'
17
+ end
@@ -0,0 +1,127 @@
1
+ AdhocScript = Struct.new( :description, :scope, :method, :params ) do
2
+ SYMBOLS = ['-', "\\", '|', '/', '-', "\\", '|', '/']
3
+ RECENT_WINDOW = 10
4
+
5
+ def initialize( *args )
6
+ super
7
+
8
+ self.method ||= :find_each # the method to call on the scope
9
+ self.params ||= [] # the arguments to @method
10
+
11
+ unless self.scope.respond_to?(:count)
12
+ raise ArgumentError.new( "#{self.scope.inspect} does not respond to `#count`" )
13
+ end
14
+ unless self.scope.respond_to?(self.method)
15
+ raise ArgumentError.new( "#{self.scope.inspect} does not respond to `##{self.method}`" )
16
+ end
17
+
18
+ @terminal_columns = `/usr/bin/env tput cols`.to_i
19
+ end
20
+
21
+ def run( blk=nil )
22
+ unless blk.respond_to?(:call) || block_given?
23
+ raise ArgumentError.new( "A block or other callable is required to #run" )
24
+ end
25
+
26
+ reset
27
+ print_progress
28
+
29
+ scope.send( method, *params ) do |*args|
30
+ begin
31
+ block_given? ?
32
+ yield( *args ) :
33
+ blk.call( *args )
34
+ @current += 1
35
+ @complete = (@current / @total.to_f) * 100
36
+ print_progress
37
+ rescue => e
38
+ print_error( e, args )
39
+ return
40
+ end
41
+ end
42
+
43
+ print_progress
44
+ logger.puts "\n"
45
+ end
46
+
47
+ @logger = $stdout
48
+ def self.logger=( logger ); @logger = logger; end
49
+ def self.logger; @logger; end
50
+ def logger; self.class.logger; end
51
+
52
+ private
53
+
54
+ def reset
55
+ @start_time = Time.now.to_i # timestamp
56
+ @symbols = SYMBOLS.dup # nice rotating symbols :)
57
+ @complete = 0.0 # completion percentage
58
+ @current = 0 # index of current item
59
+ @total = scope.count # total items to process
60
+ @recent_times = [] # last RECENT_WINDOW items process time
61
+ end
62
+
63
+ def time_remaining
64
+ now = Time.now.to_i
65
+ creep = (((now - @start_time) * 100) / @complete) - (now - @start_time)
66
+ @recent_times = (@recent_times.unshift(creep)).slice( 0, RECENT_WINDOW )
67
+ estimated_complete_time = now + (@recent_times.inject(:+) / @recent_times.size).to_i rescue nil
68
+ distance_of_time_in_words( now, estimated_complete_time )
69
+ end
70
+
71
+ def next_symbol
72
+ case completion_percentage
73
+ when 100 then return '*'
74
+ when 0 then return '-'
75
+ else return @symbols.push( @symbols.shift ).first
76
+ end
77
+ end
78
+
79
+ def distance_of_time_in_words( from, to )
80
+ return nil if to.nil? || completion_percentage == 0 || @recent_times.empty?
81
+
82
+ distance_in_seconds = (to - from).abs
83
+ distance_in_minutes = (distance_in_seconds / 60.0).round
84
+
85
+ case distance_in_minutes
86
+ when 0
87
+ case distance_in_seconds
88
+ when 0..9 then 'less than 10 seconds'
89
+ when 10..50 then "#{distance_in_seconds} seconds"
90
+ else "less than 1 minute"
91
+ end
92
+ when 1 then "1 minute"
93
+ when 2..44 then "#{distance_in_minutes} minutes"
94
+ when 45..89 then "about 1 hour"
95
+ when 90..1439 then "about #{(distance_in_minutes / 60.0).round} hours"
96
+ when 1440..2519 then "1 day"
97
+ when 2520..43199 then "#{(distance_in_minutes / 1440.0).round} days"
98
+ else "an extremely long time"
99
+ end
100
+ end
101
+
102
+ def completion_percentage
103
+ @complete.ceil.to_i
104
+ end
105
+
106
+ def print_progress
107
+ report = "#{next_symbol} #{description}: #{completion_percentage}%"
108
+ unless (tr = time_remaining).nil?
109
+ report += " (Remaining: #{tr})"
110
+ end
111
+
112
+ output = report.ljust(description.length + 50)
113
+ output = output.slice( 0, @terminal_columns - 1 ) if @terminal_columns != 0
114
+
115
+ logger.print output + "\r"
116
+ end
117
+
118
+ def print_error( e, args )
119
+ # this is just for error handling/output purposes
120
+ klass = scope.respond_to?(:klass) ? scope.klass : scope.class
121
+ logger.print "\n\n"
122
+ logger.puts "Script failed at #{completion_percentage}% on #{klass.name.to_s} with arguments: #{args.inspect}!"
123
+ logger.print "\n\n"
124
+ logger.puts e.message
125
+ logger.puts e.backtrace.join("\n")
126
+ end
127
+ end
@@ -0,0 +1,59 @@
1
+ require_relative '../lib/adhoc_script'
2
+ require_relative './fake_model'
3
+ require 'minitest/autorun'
4
+ require 'minitest/unit'
5
+
6
+ AdhocScript.logger = File.open( "/dev/null", 'w' )
7
+
8
+ describe AdhocScript do
9
+
10
+ it 'performs the base case' do
11
+ adhoc = AdhocScript.new( 'Going through stuff', FakeModel )
12
+ count = FakeModel::COUNT
13
+ adhoc.run { count -= 1 }
14
+ assert_equal 0, count
15
+ end
16
+
17
+ it 'works on other objects' do
18
+ count = 100
19
+ adhoc = AdhocScript.new( "Counting up to #{count}", (1..count), :each )
20
+ adhoc.run { count -= 1 }
21
+ assert_equal 0, count
22
+ end
23
+
24
+ it 'requires that the target responds to `#count`' do
25
+ assert_raises( ArgumentError ) do
26
+ AdhocScript.new( "Fail", 1 )
27
+ end
28
+ end
29
+
30
+ it 'requires that the target responds to the method specified' do
31
+ assert_raises( ArgumentError ) do
32
+ AdhocScript.new( "Fail", [1,2,3], :nope )
33
+ end
34
+ end
35
+
36
+ it 'requires a block (or other callable) to `#run`' do
37
+ adhoc = AdhocScript.new( "Fail", [1,2,3], :each )
38
+ assert_raises( ArgumentError ) { adhoc.run }
39
+ assert_raises( ArgumentError ) { adhoc.run( 1 ) }
40
+ end
41
+
42
+ it '`#run` accepts anything which can be `#call`ed' do
43
+ count = 100
44
+ adhoc = AdhocScript.new( "Counting up to #{count}", (1..count), :each )
45
+ callable = ->( i ) { count -= 1 }
46
+
47
+ adhoc.run( callable )
48
+ assert_equal 0, count
49
+
50
+ count = 100
51
+ adhoc.run( &callable )
52
+ assert_equal 0, count
53
+
54
+ count = 100
55
+ adhoc.run { |i| callable.(i) }
56
+ assert_equal 0, count
57
+ end
58
+
59
+ end
@@ -0,0 +1,11 @@
1
+ class FakeModel
2
+ COUNT = 10
3
+ def self.find_each( &block )
4
+ COUNT.times do
5
+ yield 1
6
+ end
7
+ end
8
+ def self.count
9
+ COUNT
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adhoc_script
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ryan Funduk
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-25 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: A simple wrapper class for running adhoc scripts on sets of data.
15
+ email:
16
+ - ryan.funduk@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - Gemfile
23
+ - LICENSE
24
+ - README.md
25
+ - Rakefile
26
+ - adhoc_script.gemspec
27
+ - lib/adhoc_script.rb
28
+ - test/adhoc_script_test.rb
29
+ - test/fake_model.rb
30
+ homepage: http://github.com/rfunduk/adhoc_script
31
+ licenses: []
32
+ post_install_message:
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubyforge_project:
50
+ rubygems_version: 1.8.23
51
+ signing_key:
52
+ specification_version: 3
53
+ summary: ''
54
+ test_files:
55
+ - test/adhoc_script_test.rb
56
+ - test/fake_model.rb