adhoc_script 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.md +84 -0
- data/Rakefile +5 -0
- data/adhoc_script.gemspec +17 -0
- data/lib/adhoc_script.rb +127 -0
- data/test/adhoc_script_test.rb +59 -0
- data/test/fake_model.rb +11 -0
- metadata +56 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/lib/adhoc_script.rb
ADDED
@@ -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
|
data/test/fake_model.rb
ADDED
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
|