jack 1.0.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.
@@ -0,0 +1,5 @@
1
+ == 1.0.0 / 2007-08-25
2
+
3
+ * 1 major enhancement
4
+ * Birthday!
5
+
@@ -0,0 +1,15 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/jack.rb
6
+ lib/jack/queues.rb
7
+ lib/jack/queues/appcast.rb
8
+ lib/jack/queues/mock.rb
9
+ lib/jack/tasks.rb
10
+ lib/jack/tasks/ffmpeg.rb
11
+ lib/jack/tasks/locking.rb
12
+ lib/jack/tasks/s3.rb
13
+ test/queues_test.rb
14
+ test/tasks_test.rb
15
+ test/test_helper.rb
@@ -0,0 +1,48 @@
1
+ jack
2
+ by FIX (your name)
3
+ FIX (url)
4
+
5
+ == DESCRIPTION:
6
+
7
+ FIX (describe your package)
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * FIX (list of features or problems)
12
+
13
+ == SYNOPSIS:
14
+
15
+ FIX (code sample of usage)
16
+
17
+ == REQUIREMENTS:
18
+
19
+ * FIX (list of requirements)
20
+
21
+ == INSTALL:
22
+
23
+ * FIX (sudo gem install, anything else)
24
+
25
+ == LICENSE:
26
+
27
+ (The MIT License)
28
+
29
+ Copyright (c) 2007 Rick Olson
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining
32
+ a copy of this software and associated documentation files (the
33
+ 'Software'), to deal in the Software without restriction, including
34
+ without limitation the rights to use, copy, modify, merge, publish,
35
+ distribute, sublicense, and/or sell copies of the Software, and to
36
+ permit persons to whom the Software is furnished to do so, subject to
37
+ the following conditions:
38
+
39
+ The above copyright notice and this permission notice shall be
40
+ included in all copies or substantial portions of the Software.
41
+
42
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
43
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
44
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
45
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
46
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
47
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
48
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,20 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ $: << 'lib'
6
+ require 'jack'
7
+
8
+ Hoe.new('jack', Jack::VERSION) do |p|
9
+ p.rubyforge_name = 'jack'
10
+ p.author = 'Rick Olson'
11
+ p.email = 'technoweenie@gmail.com'
12
+ # p.summary = 'FIX'
13
+ # p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
14
+ # p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
15
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
16
+ p.extra_deps << 'rake' << 'open4'
17
+ p.test_globs << 'test/**/*_test.rb'
18
+ end
19
+
20
+ # vim: syntax=Ruby
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'thread'
4
+ require 'jack/tasks'
5
+
6
+ module Jack
7
+ VERSION = '1.0.0'
8
+
9
+ class Task < Rake::Task
10
+ def execute
11
+ Thread.current[:task] = self
12
+ super
13
+ ensure
14
+ Thread.current[:task] = nil
15
+ end
16
+ end
17
+ end
18
+
19
+ include Jack::Tasks
@@ -0,0 +1,56 @@
1
+ module Jack
2
+ module Queues
3
+ def process_queue(queue_name, options = {}, &block)
4
+ Queues::Task.queue_task(queue_name, options, &block)
5
+ end
6
+
7
+ def queue
8
+ Thread.current[:task]
9
+ end
10
+
11
+ class Task < Jack::Task
12
+ attr_accessor :queue_name
13
+ attr_accessor :connection_args
14
+ attr_accessor :options
15
+
16
+ def kept
17
+ @kept ||= []
18
+ end
19
+
20
+ def keep(*messages)
21
+ messages.flatten!
22
+ messages.uniq!
23
+ kept.push *messages
24
+ end
25
+
26
+ def execute
27
+ task = lambda do
28
+ if messages.empty?
29
+ false
30
+ else
31
+ super
32
+ (messages - kept).each do |msg|
33
+ delete msg
34
+ end
35
+ kept.empty?
36
+ end
37
+ end
38
+ while task.call
39
+ @messages = nil
40
+ end
41
+ end
42
+
43
+ class << self
44
+ attr_accessor :default_connection_args
45
+ end
46
+
47
+ def self.queue_task(name, options = {}, &block)
48
+ task = define_task(name, &block)
49
+ task.queue_name = options.delete(:queue_name) || name
50
+ task.options = options
51
+ task.connection_args = options.delete(:connect) || default_connection_args
52
+ task
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,32 @@
1
+ require 'appcast/client'
2
+ module Jack
3
+ module Queues
4
+ module Appcast
5
+ def connection
6
+ @connection ||= ::Appcast::Client.new(*connection_args)
7
+ end
8
+
9
+ def messages
10
+ if @messages.nil?
11
+ @messages = connection.list(@queue_name, @options)
12
+ logger.info("[Appcast] Found #{@messages.size} message(s)")
13
+ end
14
+ @messages
15
+ end
16
+
17
+ def delete(message)
18
+ logger.info("[Appcast] Deleting message for #{message.name}")
19
+ message.destroy
20
+ end
21
+
22
+ def create(name_or_data, data = nil)
23
+ if data.nil?
24
+ data = name_or_data
25
+ name_or_data = @queue_name
26
+ end
27
+ logger.info("[Appcast] Created message for #{name_or_data}")
28
+ connection.create(name_or_data, data)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ module Jack
2
+ module Queues
3
+ module Mock
4
+ def connection
5
+ @connection ||= []
6
+ end
7
+
8
+ def messages
9
+ @messages ||= (connection.shift || [])
10
+ end
11
+
12
+ def delete(message)
13
+ messages.delete message
14
+ end
15
+
16
+ def create(name_or_data, data = nil)
17
+ if data.nil?
18
+ data = name_or_data
19
+ name_or_data = @queue_name
20
+ end
21
+ created << [name_or_data, data]
22
+ end
23
+
24
+ def created
25
+ @created ||= []
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ require 'logger'
2
+ require 'jack/tasks/ffmpeg'
3
+ require 'jack/tasks/locking'
4
+
5
+ module Jack
6
+ module Tasks
7
+ def setup_queue(queue_type, *args)
8
+ require "jack/queues"
9
+ require "jack/queues/#{queue_type}"
10
+ include Jack::Queues
11
+ mod = Jack::Queues.const_get(queue_type.to_s.capitalize)
12
+ Jack::Queues::Task.send :include, mod
13
+ Jack::Queues::Task.default_connection_args = args
14
+ end
15
+
16
+ def setup_s3(options)
17
+ require 'jack/tasks/s3'
18
+ @default_s3_bucket = options.delete(:bucket)
19
+ @s3_working_path = options.delete(:working).to_s
20
+ AWS::S3::Base.establish_connection!(options) if options.any?
21
+ include Jack::Tasks::S3
22
+ end
23
+
24
+ def setup_logger(*args)
25
+ @logger = Logger.new(*args)
26
+ end
27
+
28
+ def logger
29
+ @logger ||= Logger.new(STDERR)
30
+ end
31
+
32
+ include Jack::Tasks::Ffmpeg
33
+ include Jack::Tasks::Locking
34
+ end
35
+ end
@@ -0,0 +1,75 @@
1
+ require 'open4'
2
+
3
+ module Jack
4
+ module Tasks
5
+ module Ffmpeg
6
+ def ffmpeg(input, options = {})
7
+ cmd = ['ffmpeg']
8
+ input = { :file => input } unless input.is_a?(Hash)
9
+ ffmpeg_options :input, cmd, input
10
+ ffmpeg_options :output, cmd, options
11
+ execute_command cmd.join(" ")
12
+ end
13
+
14
+ def convert_to_flv(filename, size, output = nil, options = {})
15
+ if output.is_a?(Hash)
16
+ options = output
17
+ output = nil
18
+ end
19
+ default = {:rate => 25, :acodec => :mp3, :frequency => 22050, :overwrite => true, :size => size, :file => (output || filename + ".flv")}
20
+ ffmpeg filename, default.update(options)
21
+ options[:file]
22
+ end
23
+
24
+ def grab_screenshot_from(filename, size, output = nil, options = {})
25
+ if output.is_a?(Hash)
26
+ options = output
27
+ output = nil
28
+ end
29
+ default = {:vframes => 1, :format => :image2, :disable_audio => true, :size => size, :file => (output || filename + ".jpg")}
30
+ ffmpeg filename, default.update(options)
31
+ options[:file]
32
+ end
33
+
34
+ protected
35
+ def ffmpeg_options(param, cmd, options)
36
+ file = options.delete(:file)
37
+ options.inject(cmd) do |c, (key, value)|
38
+ c << \
39
+ case key
40
+ when :duration then "-t #{value}"
41
+ when :rate then "-r #{value}"
42
+ when :bitrate then "-b #{value}"
43
+ when :seek then "-ss #{value}"
44
+ when :verbose then "-v #{value == true ? :verbose : value}"
45
+ when :size then "-s #{value}"
46
+ when :overwrite then "-y"
47
+ when :format then "-f #{value}"
48
+ when :frequency then "-ar #{value}"
49
+ when :abitrate then "-ab #{value}"
50
+ when :disable_video then "-vn"
51
+ when :disable_audio then "-an"
52
+ else "-#{key} #{value}"
53
+ end
54
+ end
55
+ cmd << "#{param == :input ? '-i ' : ''}#{File.expand_path(file)}"
56
+ end
57
+
58
+ def execute_command(cmd)
59
+ logger.info "[ffmpeg] Executing: #{cmd}"
60
+ result = []
61
+ Open4.popen4 cmd do |pid, stdin, stdout, stderr|
62
+ result << stdout.read.to_s.strip
63
+ result << stderr.read.to_s.strip
64
+ end
65
+ unless result.first.blank?
66
+ logger.info "[ffmpeg] OUTPUT: #{result.first}"
67
+ end
68
+ unless result.last.blank?
69
+ logger.debug "[ffmpeg] ERROR: #{result.last}"
70
+ end
71
+ result
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,20 @@
1
+ module Jack
2
+ module Tasks
3
+ module Locking
4
+ def lock(*args)
5
+ require 'lockfile' unless Object.const_defined?(:Lockfile)
6
+ options = args.last.is_a?(Hash) ? args.pop : {:retries => 0}
7
+ lock_filename = args.shift || 'jack.lock'
8
+ if block_given?
9
+ lockfile = ::Lockfile.new(lock_filename, options)
10
+ begin
11
+ lockfile.lock
12
+ yield
13
+ ensure
14
+ lockfile.unlock if lockfile.locked?
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,44 @@
1
+ require 'aws/s3'
2
+
3
+ module Jack
4
+ module Tasks
5
+ module S3
6
+ def store_in_s3(name, filename, bucket = nil, options = {})
7
+ if bucket.is_a?(Hash)
8
+ options = bucket
9
+ bucket = nil
10
+ end
11
+ bucket ||= default_s3_bucket
12
+ logger.info("[S3] Storing #{name} in #{bucket} from #{filename}")
13
+ AWS::S3::S3Object.store name, open(filename), bucket, options
14
+ end
15
+
16
+ def download_from_s3(name, bucket = nil)
17
+ bucket ||= default_s3_bucket
18
+ logger.info("[S3] Downloading #{name} from #{bucket}")
19
+ Dir.chdir s3_working_path do
20
+ open name, 'w' do |file|
21
+ AWS::S3::S3Object.stream name, bucket do |chunk|
22
+ file.write chunk
23
+ end
24
+ end
25
+ end
26
+ File.join s3_working_path, name
27
+ end
28
+
29
+ def delete_from_s3(name, bucket = nil)
30
+ bucket ||= default_s3_bucket
31
+ logger.info("[S3] Deleting #{name} from #{bucket}")
32
+ AWS::S3::S3Object.delete name, bucket
33
+ end
34
+
35
+ def default_s3_bucket
36
+ @default_s3_bucket
37
+ end
38
+
39
+ def s3_working_path
40
+ @s3_working_path
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,71 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ context "Jack Queues" do
4
+ specify "should provide access to queue object" do
5
+ @task = JACK.process_queue :queue_object do
6
+ JACK.poke
7
+ JACK.queue.should == @task
8
+ end
9
+ JACK.expects(:poke)
10
+ @task.connection << [:msg]
11
+ @task.execute
12
+ end
13
+
14
+ specify "should create message with current queue name" do
15
+ @task = JACK.process_queue :create do
16
+ JACK.poke
17
+ JACK.queue.create :foo
18
+ end
19
+ JACK.expects(:poke)
20
+ @task.connection << [:msg]
21
+ @task.execute
22
+ @task.created.should == [[:create, :foo]]
23
+ end
24
+
25
+ specify "should create message with custom queue name" do
26
+ @task = JACK.process_queue :create_custom do
27
+ JACK.poke
28
+ JACK.queue.create :foo, :bar
29
+ end
30
+ JACK.expects(:poke)
31
+ @task.connection << [:msg]
32
+ @task.execute
33
+ @task.created.should == [[:foo, :bar]]
34
+ end
35
+
36
+ specify "should set queue name to task name" do
37
+ @task = JACK.process_queue(:queue_name) {}
38
+ @task.queue_name.should == :queue_name
39
+ end
40
+
41
+ specify "should set options and custom queue name" do
42
+ @task = JACK.process_queue(:queue_options, :queue_name => :foo, :example => :option) {}
43
+ @task.queue_name.should == :foo
44
+ @task.options.should == {:example => :option}
45
+ end
46
+
47
+ specify "should execute queue task" do
48
+ @task = JACK.process_queue :queue_task do
49
+ JACK.poke
50
+ JACK.queue.messages.should == [:msg, :keep]
51
+ JACK.queue.keep :keep
52
+ end
53
+ JACK.expects(:poke)
54
+ @task.connection << [:msg, :keep] << [:msg2] # store messages
55
+ @task.execute
56
+ end
57
+
58
+ specify "should loop queue task" do
59
+ @task = JACK.process_queue :queue_loop do
60
+ if JACK.poke == 1
61
+ JACK.queue.messages.should == [:msg, :msg2]
62
+ else
63
+ JACK.queue.messages.should == [:msg3]
64
+ end
65
+ end
66
+ JACK.expects(:poke).times(2).returns(1,2)
67
+ @task.connection << [:msg, :msg2] << [:msg3] # store messages
68
+ @task.execute
69
+ @task.messages.should.be.empty
70
+ end
71
+ end
@@ -0,0 +1,100 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ context "Jack Tasks" do
4
+ specify "should store instance in current thread while executing" do
5
+ @task = Jack::Task.define_task :test_thread do
6
+ Thread.current[:task].should == @task
7
+ end
8
+
9
+ Thread.current[:task].should.be.nil
10
+ @task.execute
11
+ Thread.current[:task].should.be.nil
12
+ end
13
+
14
+ specify "should setup s3 bucket name" do
15
+ JACK.default_s3_bucket.should == 'stuff'
16
+ end
17
+
18
+ specify "should setup s3 working path" do
19
+ JACK.s3_working_path.should == 'foo/bar'
20
+ end
21
+
22
+ specify "should set s3 connection options" do
23
+ AWS::S3::Base.connection.options[:access_key_id].should == 'abc'
24
+ AWS::S3::Base.connection.options[:secret_access_key].should == 'def'
25
+ end
26
+
27
+ specify "should store with default bucket and options" do
28
+ @stream = stub
29
+ JACK.expects(:open).with('filename').returns(@stream)
30
+ AWS::S3::S3Object.expects(:store).with('foo', @stream, 'stuff', {:expires => 300})
31
+ JACK.store_in_s3 'foo', 'filename', :expires => 300
32
+ end
33
+
34
+ specify "should store with custom bucket and options" do
35
+ @stream = stub
36
+ JACK.expects(:open).with('filename').returns(@stream)
37
+ AWS::S3::S3Object.expects(:store).with('foo', @stream, 'things', {:expires => 300})
38
+ JACK.store_in_s3 'foo', 'filename', 'things', :expires => 300
39
+ end
40
+
41
+ specify "should download from s3 from default bucket" do
42
+ @stream = StringIO.new
43
+ Dir.expects(:chdir).with(JACK.s3_working_path).yields
44
+ JACK.expects(:open).with('foo', 'w').yields(@stream)
45
+ AWS::S3::S3Object.expects(:stream).with('foo', 'stuff').yields("content")
46
+ JACK.download_from_s3 'foo'
47
+ @stream.rewind
48
+ @stream.read.should == 'content'
49
+ end
50
+
51
+ specify "should download from s3 from custom bucket" do
52
+ @stream = StringIO.new
53
+ Dir.expects(:chdir).with(JACK.s3_working_path).yields
54
+ JACK.expects(:open).with('foo', 'w').yields(@stream)
55
+ AWS::S3::S3Object.expects(:stream).with('foo', 'things').yields("content")
56
+ JACK.download_from_s3 'foo', 'things'
57
+ @stream.rewind
58
+ @stream.read.should == 'content'
59
+ end
60
+
61
+ specify "should delete with default s3 bucket" do
62
+ AWS::S3::S3Object.expects(:delete).with('foo', 'stuff')
63
+ JACK.delete_from_s3 'foo'
64
+ end
65
+
66
+ specify "should delete with custom s3 bucket" do
67
+ AWS::S3::S3Object.expects(:delete).with('foo', 'things')
68
+ JACK.delete_from_s3 'foo', 'things'
69
+ end
70
+
71
+ specify "should create ffmpeg command" do
72
+ File.expects(:expand_path).with('foo').returns('foo_path')
73
+ File.expects(:expand_path).with('bar').returns('bar_path')
74
+ JACK.ffmpeg('foo', :file => 'bar').should == "ffmpeg -i foo_path bar_path"
75
+ end
76
+
77
+ {
78
+ [:duration, 3] => [:t],
79
+ [:rate, 3] => [],
80
+ [:seek, 3] => [:ss],
81
+ [:verbose, true] => [:v, :verbose],
82
+ [:size, 3] => [],
83
+ [:overwrite, nil] => [:y],
84
+ [:format, :test] => [:f],
85
+ [:frequency, :test] => [:ar],
86
+ [:abitrate, :test] => [:ab],
87
+ [:disable_video, nil] => [:vn],
88
+ [:disable_audio, nil] => [:an],
89
+ [:whatever, :test] => [:whatever]
90
+ }.each do |args, expected|
91
+ specify "should recognize ffmpeg argument -#{args[0]}" do
92
+ File.expects(:expand_path).with('foo').returns('foo_path')
93
+ File.expects(:expand_path).with('bar').returns('bar_path')
94
+ switch = expected[0] || args[0].to_s[0..0]
95
+ value = args[1] ? " #{expected[1] || args[1]}" : nil
96
+ JACK.ffmpeg('foo', args[0] => args[1], :file => 'bar').should == "ffmpeg -i foo_path -#{switch}#{value} bar_path"
97
+ end
98
+ end
99
+ end
100
+
@@ -0,0 +1,25 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
2
+ require 'test/unit'
3
+ require 'jack'
4
+ require 'test/spec'
5
+ require 'mocha'
6
+
7
+ JACK = Object.new
8
+ JACK.extend Jack::Tasks
9
+ class << JACK
10
+ def include(*args)
11
+ extend(*args)
12
+ end
13
+
14
+ def logger_stream
15
+ @logger_stream ||= StringIO.new
16
+ end
17
+
18
+ def execute_command(cmd)
19
+ cmd
20
+ end
21
+ end
22
+
23
+ JACK.setup_queue :mock
24
+ JACK.setup_s3 :bucket => 'stuff', :working => 'foo/bar', :access_key_id => 'abc', :secret_access_key => 'def'
25
+ JACK.setup_logger JACK.logger_stream
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.4
3
+ specification_version: 1
4
+ name: jack
5
+ version: !ruby/object:Gem::Version
6
+ version: 1.0.0
7
+ date: 2007-09-07 00:00:00 -05:00
8
+ summary: The author was too lazy to write a summary
9
+ require_paths:
10
+ - lib
11
+ email: technoweenie@gmail.com
12
+ homepage: http://www.zenspider.com/ZSS/Products/jack/
13
+ rubyforge_project: jack
14
+ description: The author was too lazy to write a description
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Rick Olson
31
+ files:
32
+ - History.txt
33
+ - Manifest.txt
34
+ - README.txt
35
+ - Rakefile
36
+ - lib/jack.rb
37
+ - lib/jack/queues.rb
38
+ - lib/jack/queues/appcast.rb
39
+ - lib/jack/queues/mock.rb
40
+ - lib/jack/tasks.rb
41
+ - lib/jack/tasks/ffmpeg.rb
42
+ - lib/jack/tasks/locking.rb
43
+ - lib/jack/tasks/s3.rb
44
+ - test/queues_test.rb
45
+ - test/tasks_test.rb
46
+ - test/test_helper.rb
47
+ test_files:
48
+ - test/test_helper.rb
49
+ - test/queues_test.rb
50
+ - test/tasks_test.rb
51
+ rdoc_options:
52
+ - --main
53
+ - README.txt
54
+ extra_rdoc_files:
55
+ - History.txt
56
+ - Manifest.txt
57
+ - README.txt
58
+ executables: []
59
+
60
+ extensions: []
61
+
62
+ requirements: []
63
+
64
+ dependencies:
65
+ - !ruby/object:Gem::Dependency
66
+ name: rake
67
+ version_requirement:
68
+ version_requirements: !ruby/object:Gem::Version::Requirement
69
+ requirements:
70
+ - - ">"
71
+ - !ruby/object:Gem::Version
72
+ version: 0.0.0
73
+ version:
74
+ - !ruby/object:Gem::Dependency
75
+ name: open4
76
+ version_requirement:
77
+ version_requirements: !ruby/object:Gem::Version::Requirement
78
+ requirements:
79
+ - - ">"
80
+ - !ruby/object:Gem::Version
81
+ version: 0.0.0
82
+ version:
83
+ - !ruby/object:Gem::Dependency
84
+ name: hoe
85
+ version_requirement:
86
+ version_requirements: !ruby/object:Gem::Version::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 1.3.0
91
+ version: