azkaban_scheduler 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bb696acb2c67ae0e468b3289302b4066dea786a9
4
+ data.tar.gz: c9be59214ff6f278d726a8475143634c85fe4734
5
+ SHA512:
6
+ metadata.gz: a700b32c8abb25fe772212a55f6a568deb46cb1399e7da86c5fc2a52bad57b712da93e249ae411abed96c9e14eed8a4cc0b14f68d7a72ef185470bcb7f541d31
7
+ data.tar.gz: ade57a7249c77c9ae77d245fdbaa6cad406b60769398ab5accf537c8260d17ea3073086ac681c15d730470547a514e90a66036b207a0717870c1825cb8e99773
data/.gitignore ADDED
@@ -0,0 +1,24 @@
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
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ test/azkaban.yml
24
+ .ruby-version
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Dylan Thacker-Smith
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # AzkabanScheduler
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'azkaban_scheduler'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install azkaban_scheduler
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it ( https://github.com/[my-github-username]/azkaban_scheduler/fork )
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ task :default => 'test'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << '.' << 'lib' << 'test'
8
+ t.test_files = FileList['test/**/*_test.rb']
9
+ t.verbose = false
10
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'azkaban_scheduler/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "azkaban_scheduler"
8
+ spec.version = AzkabanScheduler::VERSION
9
+ spec.authors = ["Dylan Thacker-Smith"]
10
+ spec.email = ["Dylan.Smith@shopify.com"]
11
+ spec.summary = "Azkaban client that can update the schedule"
12
+ spec.homepage = "https://github.com/dylanahsmith/azkaban_scheduler"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency("rubyzip", "~> 1.1")
21
+ spec.add_dependency("multipart-post", "~> 2.0")
22
+
23
+ spec.add_development_dependency("bundler", "~> 1.6")
24
+ spec.add_development_dependency("rake")
25
+ spec.add_development_dependency("minitest", "~> 5.4")
26
+ end
@@ -0,0 +1,6 @@
1
+ require 'azkaban_scheduler/errors'
2
+ require 'azkaban_scheduler/client'
3
+ require 'azkaban_scheduler/job'
4
+ require 'azkaban_scheduler/project'
5
+ require 'azkaban_scheduler/session'
6
+ require 'azkaban_scheduler/version'
@@ -0,0 +1,49 @@
1
+ require 'net/http'
2
+ require 'json'
3
+
4
+ module AzkabanScheduler
5
+ class Client
6
+ def initialize(url)
7
+ uri = URI(url)
8
+ @http = Net::HTTP.new(uri.host, uri.port)
9
+ @http.use_ssl = uri.scheme == 'https'
10
+ end
11
+
12
+ def get(path, params=nil, headers=nil)
13
+ path += "?#{URI.encode_www_form(params)}" if params
14
+ req = Net::HTTP::Get.new(path)
15
+ send_request(req, headers)
16
+ end
17
+
18
+ def post(path, params=nil, headers=nil)
19
+ req = Net::HTTP::Post.new(path)
20
+ req.set_form_data(params) if params
21
+ send_request(req, headers)
22
+ end
23
+
24
+ def multipart_post(path, params, headers=nil)
25
+ req = Net::HTTP::Post::Multipart.new(path, params)
26
+ send_request(req, headers)
27
+ end
28
+
29
+ private
30
+
31
+ def send_request(request, headers)
32
+ request['Accept'] = 'application/json'
33
+ headers.each { |name, value| request[name] = value } if headers
34
+ response = @http.request(request)
35
+ dump_response(response) if ENV['DUMP_AZKABAN_RESPONSES']
36
+ response
37
+ end
38
+
39
+ def dump_response(response)
40
+ puts "HTTP/#{response.http_version} #{response.code} #{response.message}"
41
+ response.each_header do |name, value|
42
+ puts "#{name}: #{value}"
43
+ end
44
+ puts
45
+ puts "#{response.body}"
46
+ puts "-" * 60
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,8 @@
1
+ module AzkabanScheduler
2
+ class AzkabanError < StandardError; end
3
+ class AuthenticationError < AzkabanError; end
4
+ class ProjectNotFoundError < AzkabanError; end
5
+ class ProjectExistsError < AzkabanError; end
6
+ class InvalidProjectNameError < AzkabanError; end
7
+ class ProjectDescriptionEmptyError < AzkabanError; end
8
+ end
@@ -0,0 +1,13 @@
1
+ module AzkabanScheduler
2
+ class Job
3
+ def initialize(params)
4
+ @params = params
5
+ end
6
+
7
+ def write(io)
8
+ @params.each do |key, value|
9
+ io.puts("#{key}=#{value}")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,35 @@
1
+ require 'stringio'
2
+ require 'zip'
3
+
4
+ module AzkabanScheduler
5
+ class Project
6
+ attr_accessor :name, :description, :jobs
7
+ attr_accessor :id, :version
8
+
9
+ def initialize(name, description)
10
+ @name = name
11
+ @description = description
12
+ @jobs = {}
13
+ end
14
+
15
+ def add_job(name, job)
16
+ @jobs[name] = job
17
+ end
18
+
19
+ def build
20
+ io = StringIO.new
21
+ write(io)
22
+ io.rewind
23
+ io
24
+ end
25
+
26
+ def write(io)
27
+ Zip::OutputStream.write_buffer(io) do |out|
28
+ @jobs.each do |name, job|
29
+ out.put_next_entry("#{name}.job")
30
+ job.write(out)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,16 @@
1
+ module AzkabanScheduler
2
+ class Schedule
3
+ Stats = Struct.new(:min, :max, :average)
4
+ attr_accessor :id, :stats
5
+
6
+ def initialize(project_name, flow_name, options={})
7
+ @project_name = project_name
8
+ @flow_name = flow_name
9
+ @period = options[:period]
10
+ @is_recurring = !!options[:period]
11
+ @time = options[:time] || Time.now.to_i
12
+ @failure_emails = options[:failure_emails]
13
+ @success_emails = options[:success_emails]
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,163 @@
1
+ require 'cgi'
2
+ require 'net/http/post/multipart'
3
+
4
+ module AzkabanScheduler
5
+ class Session
6
+ attr_accessor :id
7
+
8
+ def self.start(client, username, password)
9
+ params = {'action' => 'login', 'username' => username, 'password' => password}
10
+ response = client.post('/', params)
11
+ response.error! unless response.kind_of?(Net::HTTPSuccess)
12
+ result = JSON.parse(response.body)
13
+ unless result["status"] == "success"
14
+ error_message = result["error"]
15
+ if error_message == "Incorrect Login. Username\/Password not found."
16
+ raise AuthenticationError, error_message
17
+ end
18
+ raise AzkabanError, error_message
19
+ end
20
+ new(client, result['session.id'])
21
+ end
22
+
23
+ def initialize(client, id)
24
+ @client = client
25
+ @id = id
26
+ end
27
+
28
+ def create_project(project)
29
+ response = @client.post('/manager', {
30
+ 'session.id' => @id,
31
+ 'action' => 'create',
32
+ 'name' => project.name,
33
+ 'description' => project.description,
34
+ })
35
+ response.error! unless response.kind_of?(Net::HTTPSuccess)
36
+ result = JSON.parse(response.body)
37
+ if result['status'] != 'success'
38
+ error_message = result['message']
39
+ if error_message == "Active project with name #{project.name} already exists in db."
40
+ raise ProjectExistsError, error_message
41
+ elsif error_message == "Project names must start with a letter, followed by any number of letters, digits, '-' or '_'."
42
+ raise InvalidProjectNameError, error_message
43
+ elsif error_message == "Description cannot be empty."
44
+ raise ProjectDescriptionEmptyError, error_message
45
+ end
46
+ raise AzkabanError, error_message
47
+ end
48
+ result
49
+ end
50
+
51
+ def upload_project(project)
52
+ response = @client.multipart_post('/manager', {
53
+ 'session.id' => @id,
54
+ 'ajax' => 'upload',
55
+ 'project' => project.name,
56
+ 'file' => UploadIO.new(project.build, 'application/zip', 'file.zip'),
57
+ })
58
+ response.error! unless response.kind_of?(Net::HTTPSuccess)
59
+ result = JSON.parse(response.body)
60
+ if error_message = result['error']
61
+ if error_message == "Installation Failed. Project '#{project.name}' doesn't exist."
62
+ raise ProjectNotFoundError, error_message
63
+ end
64
+ raise AzkabanError, error_message
65
+ end
66
+ project.id = result['projectId']
67
+ project.version = result['version']
68
+ result
69
+ end
70
+
71
+ def delete_project(project_name)
72
+ response = @client.get('/manager', {
73
+ 'session.id' => @id,
74
+ 'project' => project_name,
75
+ 'delete' => 'true',
76
+ })
77
+ response.error! unless response.kind_of?(Net::HTTPSuccess) || response.kind_of?(Net::HTTPRedirection)
78
+ cookies = response_cookies(response)
79
+ unless cookies['azkaban.success.message']
80
+ error_message = cookies['azkaban.failure.message']
81
+ if error_message == "Project #{project_name} doesn't exist."
82
+ return false
83
+ end
84
+ raise AzkabanError, error_message
85
+ end
86
+ true
87
+ end
88
+
89
+ def list_schedules
90
+ response = @client.post('/schedule', { 'ajax' => 'loadFlow' }, session_id_cookie)
91
+ response.error! unless response.kind_of?(Net::HTTPSuccess)
92
+ result = JSON.parse(response.body)
93
+ result['items'] || []
94
+ end
95
+
96
+ def remove_schedule(schedule_id)
97
+ response = @client.post('/schedule', {
98
+ 'action' => 'removeSched',
99
+ 'scheduleId' => schedule_id,
100
+ }, session_id_cookie)
101
+ response.error! unless response.kind_of?(Net::HTTPSuccess)
102
+ result = JSON.parse(response.body)
103
+ unless result['status'] == 'success'
104
+ error_message = result['message']
105
+ return false if error_message == "Schedule with ID #{schedule_id} does not exist"
106
+ raise AzkabanError, error_message
107
+ end
108
+ true
109
+ end
110
+
111
+ def remove_all_schedules(project_name)
112
+ list_schedules.each do |schedule|
113
+ next unless schedule['projectname'] == project_name
114
+ schedule_id = schedule['scheduleid']
115
+ remove_schedule(schedule_id)
116
+ end
117
+ end
118
+
119
+ def post_schedule(project_id, project_name, flow, start_time, options={})
120
+ response = @client.post('/schedule', {
121
+ 'ajax' => 'scheduleFlow',
122
+ 'project' => project_name,
123
+ 'projectName' => project_name,
124
+ 'projectId' => project_id,
125
+ 'flow' => flow,
126
+ 'disabled' => options[:disabled] || '[]',
127
+ 'period' => options[:period] || "1d",
128
+ 'scheduleTime' => start_time.utc.strftime("%I,%M,%p,UTC"),
129
+ 'scheduleDate' => start_time.utc.strftime("%m/%d/%Y"),
130
+ 'is_recurring' => options[:period] ? 'on' : 'off',
131
+ 'concurrentOption' => options[:concurrent_option] || 'skip',
132
+ 'failureEmailsOverride' => (!!options[:failure_emails_override]).to_s,
133
+ 'successEmailsOverride' => (!!options[:success_emails_override]).to_s,
134
+ 'failureAction' => options[:failure_action] || 'finishCurrent',
135
+ 'failureEmails' => Array(options[:failure_emails]).join(', '),
136
+ 'successEmails' => Array(options[:success_emails]).join(', '),
137
+ 'notifyFailureFirst' => (!!options[:notify_failure_first]).to_s,
138
+ 'notifyFailureLast' => (!!options[:notify_failure_last]).to_s,
139
+ }, session_id_cookie)
140
+ response.error! unless response.kind_of?(Net::HTTPSuccess)
141
+ result = JSON.parse(response.body)
142
+ unless result['status'] == 'success'
143
+ raise AzkabanError, result['message']
144
+ end
145
+ nil
146
+ end
147
+
148
+ private
149
+
150
+ def response_cookies(response)
151
+ response['Set-Cookie'].split(',').each_with_object({}) do |cookie, hash|
152
+ name, value = cookie.split(';')[0].split('=', 2)
153
+ value = value.to_s.strip
154
+ value = value[1...-1] if value[0] == '"'.freeze && value[-1] == '"'.freeze
155
+ hash[name.strip] = value
156
+ end
157
+ end
158
+
159
+ def session_id_cookie
160
+ { 'Cookie' => "azkaban.browser.session.id=#{@id}" }
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,3 @@
1
+ module AzkabanScheduler
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,88 @@
1
+ require 'test_helper'
2
+
3
+ class SessionTest < Minitest::Test
4
+ include SessionTestHelper
5
+
6
+ def setup
7
+ @project = AzkabanScheduler::Project.new('Azkaban_Scheduler_Test', 'Used by the test suite')
8
+ @project.add_job('first', AzkabanScheduler::Job.new(type: 'command', command: 'echo "hello world"'))
9
+ @@once ||= setup_once || true
10
+ end
11
+
12
+ def setup_once
13
+ session.remove_all_schedules(@project.name)
14
+ session.delete_project(@project.name)
15
+ end
16
+
17
+ def test_start
18
+ refute session.id.empty?
19
+ end
20
+
21
+ def test_start_with_incorrect_password
22
+ assert_raises(AzkabanScheduler::AuthenticationError) do
23
+ AzkabanScheduler::Session.start(client, 'foo', 'bar')
24
+ end
25
+ end
26
+
27
+ def test_create_and_upload_project
28
+ session.create_project(@project)
29
+ result = session.upload_project(@project)
30
+ assert result['projectId']
31
+ assert result['version']
32
+ assert_equal result['projectId'], @project.id
33
+ assert_equal result['version'], @project.version
34
+ assert_equal true, session.delete_project(@project.name)
35
+ end
36
+
37
+ def test_post_schedule
38
+ session.create_project(@project)
39
+ session.upload_project(@project)
40
+ flow_name = 'first'
41
+ start_time = Time.now
42
+ session.post_schedule(@project.id, @project.name, flow_name, start_time,
43
+ period: '6h',
44
+ failure_emails_override: true,
45
+ notifyFailureFirst: true,
46
+ failure_emails: ['azkaban-scheduler-test@localhost'])
47
+ schedule = session.list_schedules.detect { |item| item['projectname'] == @project.name && item['flowname'] == flow_name }
48
+ assert schedule['scheduleid']
49
+ assert_equal (start_time.to_i - start_time.sec) * 1000, schedule['time']
50
+ assert_equal 6 * 60 * 60 * 1000, schedule['period']
51
+ ensure
52
+ session.remove_all_schedules(@project.name)
53
+ session.delete_project(@project.name)
54
+ end
55
+
56
+ def test_create_existing_project
57
+ session.create_project(@project)
58
+ assert_raises(AzkabanScheduler::ProjectExistsError) do
59
+ session.create_project(@project)
60
+ end
61
+ ensure
62
+ session.delete_project(@project.name)
63
+ end
64
+
65
+ def test_create_project_with_invalid_name
66
+ project = AzkabanScheduler::Project.new('Azkaban Scheduler Test', "description")
67
+ assert_raises(AzkabanScheduler::InvalidProjectNameError) do
68
+ session.create_project(project)
69
+ end
70
+ end
71
+
72
+ def test_create_project_with_invalid_name
73
+ project = AzkabanScheduler::Project.new('Azkaban_Scheduler_Test', "")
74
+ assert_raises(AzkabanScheduler::ProjectDescriptionEmptyError) do
75
+ session.create_project(project)
76
+ end
77
+ end
78
+
79
+ def test_upload_missing_project
80
+ assert_raises(AzkabanScheduler::ProjectNotFoundError) do
81
+ session.upload_project(@project)
82
+ end
83
+ end
84
+
85
+ def test_delete_missing_project
86
+ assert_equal false, session.delete_project(@project.name)
87
+ end
88
+ end
@@ -0,0 +1,20 @@
1
+ require 'azkaban_scheduler'
2
+
3
+ require "minitest/autorun"
4
+ require 'yaml'
5
+ require 'pathname'
6
+
7
+
8
+ module SessionTestHelper
9
+ def config
10
+ @@config ||= YAML.load(Pathname.new(File.dirname(__FILE__)).join('azkaban.yml').read)
11
+ end
12
+
13
+ def client
14
+ @@client ||= AzkabanScheduler::Client.new(config['url'])
15
+ end
16
+
17
+ def session
18
+ @@session ||= AzkabanScheduler::Session.start(client, config['username'], config['password'])
19
+ end
20
+ end
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: azkaban_scheduler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Dylan Thacker-Smith
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rubyzip
15
+ version_requirements: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ requirement: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ~>
23
+ - !ruby/object:Gem::Version
24
+ version: '1.1'
25
+ prerelease: false
26
+ type: :runtime
27
+ - !ruby/object:Gem::Dependency
28
+ name: multipart-post
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: '2.0'
39
+ prerelease: false
40
+ type: :runtime
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.6'
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ~>
51
+ - !ruby/object:Gem::Version
52
+ version: '1.6'
53
+ prerelease: false
54
+ type: :development
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ prerelease: false
68
+ type: :development
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '5.4'
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ~>
79
+ - !ruby/object:Gem::Version
80
+ version: '5.4'
81
+ prerelease: false
82
+ type: :development
83
+ description:
84
+ email:
85
+ - Dylan.Smith@shopify.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - .gitignore
91
+ - Gemfile
92
+ - LICENSE.txt
93
+ - README.md
94
+ - Rakefile
95
+ - azkaban_scheduler.gemspec
96
+ - lib/azkaban_scheduler.rb
97
+ - lib/azkaban_scheduler/client.rb
98
+ - lib/azkaban_scheduler/errors.rb
99
+ - lib/azkaban_scheduler/job.rb
100
+ - lib/azkaban_scheduler/project.rb
101
+ - lib/azkaban_scheduler/schedule.rb
102
+ - lib/azkaban_scheduler/session.rb
103
+ - lib/azkaban_scheduler/version.rb
104
+ - test/session_test.rb
105
+ - test/test_helper.rb
106
+ homepage: https://github.com/dylanahsmith/azkaban_scheduler
107
+ licenses:
108
+ - MIT
109
+ metadata: {}
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 2.1.9
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Azkaban client that can update the schedule
130
+ test_files:
131
+ - test/session_test.rb
132
+ - test/test_helper.rb