tq 0.1.3

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fc0bd8304fbc66b4f921997f047c637f579250b9
4
+ data.tar.gz: d694a8a9b632e17715d78d32a7a2311176d4fed8
5
+ SHA512:
6
+ metadata.gz: f0062de8772a7555928676469d0fa9191c9d36429f0b451d8cdf28ff0225e07d8eac29d69b826f74636110639936209dda94908e9df2d18ddaf9dafcc68076c7
7
+ data.tar.gz: 370c3621670e588217dd84fea9087d94fda54c040bc8776c55ea1f592383835f32c6d0baf366c3ce5052e0f655d729b59e88b0fe3af1069cdb5de706b4ac0983
data/.gems ADDED
@@ -0,0 +1,3 @@
1
+ google-api-client -v 0.8.4
2
+ minitest -v 5.5.1
3
+ parallel -v 1.4.1
data/lib/tq.rb ADDED
@@ -0,0 +1,5 @@
1
+ require_relative 'version'
2
+ require_relative 'tq/app'
3
+ require_relative 'tq/queue'
4
+ require_relative 'tq/logger'
5
+
data/lib/tq/app.rb ADDED
@@ -0,0 +1,167 @@
1
+ require 'google/api_client'
2
+ require 'google/api_client/client_secrets'
3
+ require 'google/api_client/auth/file_storage'
4
+ require 'google/api_client/auth/installed_app'
5
+ require 'parallel'
6
+
7
+ require_relative 'queue'
8
+
9
+ TASKQUEUE_API = 'taskqueue'
10
+ TASKQUEUE_API_VERSION = 'v1beta2'
11
+ TASKQUEUE_API_SCOPES = ['https://www.googleapis.com/auth/taskqueue']
12
+
13
+ module TQ
14
+
15
+ DEFAULT_OPTIONS = {
16
+ 'concurrency' => 2,
17
+ 'log' => {
18
+ 'file' => $stderr
19
+ },
20
+ 'env' => {}
21
+ }
22
+
23
+ class App
24
+
25
+ attr_reader :id, :worker
26
+ def initialize(id, worker, options={})
27
+ @id = id; @worker = worker
28
+ @options = DEFAULT_OPTIONS.merge(options)
29
+ end
30
+
31
+ def options(_)
32
+ App.new @id, @worker, @options.merge(_)
33
+ end
34
+
35
+ def project(_)
36
+ options({'project' => _})
37
+ end
38
+
39
+ def log(_)
40
+ options({'log' => @options['log'].merge(_)})
41
+ end
42
+
43
+ def logger(_)
44
+ options({'logger' => _})
45
+ end
46
+
47
+ def env(_)
48
+ options({'env' => @options['env'].merge(_)})
49
+ end
50
+
51
+ def stdin(_)
52
+ return stdin({'name' => _}) if String === _
53
+ options({'stdin' => _})
54
+ end
55
+
56
+ def stdout(_)
57
+ return stdout({'name' => _}) if String === _
58
+ options({'stdout' => _})
59
+ end
60
+
61
+ def stderr(_)
62
+ return stderr({'name' => _}) if String === _
63
+ options({'stderr' => _})
64
+ end
65
+
66
+ def run!(secrets_file=nil, store_file=nil)
67
+ setup_logger!
68
+ _run *(_queues( TQ::Queue.new( *(auth!(secrets_file, store_file)) ).project(@options['project']) ) )
69
+ end
70
+
71
+
72
+ def service_auth!(issuer, p12_file)
73
+ key = Google::APIClient::KeyUtils.load_from_pkcs12(p12_file, 'notasecret')
74
+ client.authorization = Signet::OAuth2::Client.new(
75
+ :token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
76
+ :audience => 'https://accounts.google.com/o/oauth2/token',
77
+ :scope => TASKQUEUE_API_SCOPES,
78
+ :issuer => issuer,
79
+ :signing_key => key)
80
+ client.authorization.fetch_access_token!
81
+
82
+ api = client.discovered_api(TASKQUEUE_API, TASKQUEUE_API_VERSION)
83
+
84
+ return client, api
85
+ end
86
+
87
+ def auth!(secrets_file=nil, store_file=nil)
88
+ if store_file.nil? || (cred_store = credentials_store(store_file)).authorization.nil?
89
+ client_secrets = Google::APIClient::ClientSecrets.load(secrets_file)
90
+ flow = Google::APIClient::InstalledAppFlow.new(
91
+ :client_id => client_secrets.client_id,
92
+ :client_secret => client_secrets.client_secret,
93
+ :scope => TASKQUEUE_API_SCOPES
94
+ )
95
+ client.authorization = store_file.nil? ?
96
+ flow.authorize :
97
+ flow.authorize(cred_store)
98
+ else
99
+ client.authorization = cred_store.authorization
100
+ end
101
+
102
+ api = client.discovered_api(TASKQUEUE_API, TASKQUEUE_API_VERSION)
103
+
104
+ return client, api
105
+ end
106
+
107
+ def application_name
108
+ @id.split('/')[0]
109
+ end
110
+
111
+ def application_version
112
+ @id.split('/')[1] || '0.0.0'
113
+ end
114
+
115
+ private
116
+
117
+ def setup_logger!
118
+ if logger = @options['logger']
119
+ else
120
+ if (log = @options['log']) && (file = log['file'])
121
+ logger = Logger.new(file)
122
+ if level = log['level']
123
+ logger.level = level
124
+ end
125
+ end
126
+ end
127
+ (Google::APIClient.logger = logger) if logger
128
+ end
129
+
130
+ def client
131
+ @client ||= Google::APIClient.new(
132
+ :application_name => application_name,
133
+ :application_version => application_version
134
+ )
135
+ end
136
+
137
+ def credentials_store(file)
138
+ Google::APIClient::FileStorage.new(file)
139
+ end
140
+
141
+ def _queues(q)
142
+ qin = @options['stdin'] && q.options(@options['stdin'])
143
+ qout = @options['stdout'] && q.options(@options['stdout'])
144
+ qerr = @options['stderr'] && q.options(@options['stderr'])
145
+ return qin, qout, qerr
146
+ end
147
+
148
+ # TODO handle uncaught worker errors by qerr.push!(err) and qin.finish!(task)
149
+ # TODO raise if not qin
150
+ def _run(qin, qout, qerr)
151
+ tasks = qin.lease!
152
+ Parallel.each(tasks, :in_threads => @options['concurrency']) do |task|
153
+ @worker.new(qin, qout, qerr, inherited_env).call(task)
154
+ end
155
+ end
156
+
157
+ # default log/logger options into env
158
+ def inherited_env
159
+ env = @options['env']
160
+ log = @options['log']
161
+ logger = @options['logger']
162
+ {'log' => log, 'logger' => logger}.merge(env)
163
+ end
164
+
165
+ end
166
+
167
+ end
data/lib/tq/logger.rb ADDED
@@ -0,0 +1,93 @@
1
+ require 'logger'
2
+ require 'time'
3
+
4
+ module TQ
5
+
6
+ class Logger
7
+
8
+ DEFAULT_OPTIONS = {
9
+ 'file' => $stderr,
10
+ 'level' => ::Logger::WARN
11
+ }
12
+
13
+ def initialize(queue, options={})
14
+ @queue = queue
15
+ if Hash === options
16
+ @log = build_log( DEFAULT_OPTIONS.merge(options) )
17
+ else
18
+ @log = options
19
+ end
20
+ end
21
+
22
+ def level
23
+ @log.level
24
+ end
25
+
26
+ def level=(severity)
27
+ @log.level = severity
28
+ end
29
+
30
+ def progname
31
+ @log.progname
32
+ end
33
+
34
+ def progname=(name)
35
+ @log.progname = name
36
+ end
37
+
38
+ def add(severity, message=nil, progname=nil, context=nil, &block)
39
+ t = Time.now
40
+ @log.add(severity, message, progname, &block)
41
+ @queue.push!(
42
+ queue_message(t, severity, message, progname, context, &block),
43
+ ::Logger::SEV_LABEL[severity].to_s.downcase
44
+ ) if (severity >= level)
45
+ end
46
+
47
+ alias log add
48
+
49
+ ::Logger::SEV_LABEL.each_with_index do |label,level|
50
+ define_method(label == 'ANY' ? 'unknown' : label.to_s.downcase) do |progname=nil, context=nil, &block|
51
+ add(level, nil, progname, context, &block)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def build_log(options)
58
+ return options if options.respond_to?(:log)
59
+ logger = ::Logger.new(options['file'], options['shift_age'], options['shift_size'])
60
+ logger.level = options['level'] if options['level']
61
+ logger.progname = options['progname'] if options['progname']
62
+ return logger
63
+ end
64
+
65
+ # damn, the ruby logger interface is weird... this logic is copied almost verbatim
66
+ # from ::Logger.add
67
+ def queue_message(t, severity, message, progname, context, &block)
68
+ severity ||= ::Logger::UNKNOWN
69
+ progname ||= self.progname
70
+ if message.nil?
71
+ if block_given?
72
+ message = yield
73
+ else
74
+ message = progname
75
+ progname = self.progname
76
+ end
77
+ end
78
+
79
+ return {
80
+ time: t.iso8601,
81
+ timestamp: t.to_i,
82
+ level: severity,
83
+ label: ::Logger::SEV_LABEL[severity],
84
+ message: message,
85
+ progname: progname,
86
+ context: context
87
+ }
88
+ end
89
+
90
+ end
91
+
92
+ end
93
+
data/lib/tq/queue.rb ADDED
@@ -0,0 +1,138 @@
1
+
2
+ module TQ
3
+
4
+ class Queue
5
+
6
+ DEFAULT_OPTIONS = { 'lease_secs' => 60, 'num_tasks' => 1 }
7
+
8
+ attr_reader :client, :api
9
+ def initialize(client, api, options={})
10
+ @client, @api = client, api
11
+ @options = DEFAULT_OPTIONS.merge(options)
12
+ end
13
+
14
+ def options(_)
15
+ Queue.new @client, @api, @options.merge(_)
16
+ end
17
+
18
+ def project(_)
19
+ options({'project' => _})
20
+ end
21
+
22
+ def name(_)
23
+ options({'name' => _})
24
+ end
25
+
26
+ def lease!(opts={})
27
+ opts = @options.merge(opts)
28
+ results = client.execute!(
29
+ :api_method => api.tasks.lease,
30
+ :parameters => { :leaseSecs => opts['lease_secs'],
31
+ :project => opts['project'],
32
+ :taskqueue => opts['name'],
33
+ :numTasks => opts['num_tasks']
34
+ }
35
+ )
36
+ items = (results.data && results.data['items']) || []
37
+ items.map {|t| new_task(t) }
38
+ end
39
+
40
+ # note: does not currently work; filed bug report https://code.google.com/p/googleappengine/issues/detail?id=11838
41
+ def extend!(task, secs=nil)
42
+ secs = secs.nil? ? @options['lease_secs'] : secs
43
+ opts = @options
44
+ results = client.execute!(
45
+ :api_method => api.tasks.update,
46
+ :parameters => { :newLeaseSeconds => secs,
47
+ :project => opts['project'],
48
+ :taskqueue => opts['name'],
49
+ :task => task.id
50
+ }
51
+ )
52
+ new_task(results.data)
53
+ end
54
+
55
+ def push!(payload, tag=nil)
56
+ opts = @options
57
+ body = { 'queueName' => opts['name'],
58
+ 'payloadBase64' => encode(payload)
59
+ }
60
+ body['tag'] = tag if tag
61
+
62
+ results = client.execute!(
63
+ :api_method => api.tasks.insert,
64
+ :parameters => { :project => opts['project'],
65
+ :taskqueue => opts['name']
66
+ },
67
+ :body_object => body
68
+ )
69
+ new_task(results.data)
70
+ end
71
+
72
+ # note: you must have previously leased given task
73
+ def finish!(task)
74
+ opts = @options
75
+ client.execute!( :api_method => api.tasks.delete,
76
+ :parameters => { :project => opts['project'],
77
+ :taskqueue => opts['name'],
78
+ :task => task.id
79
+ }
80
+ )
81
+ return
82
+ end
83
+
84
+ private
85
+
86
+ def new_task(t)
87
+ Task.new(self, t['id'], timestamp_time(t['leaseTimestamp']), decode(t.payloadBase64), t)
88
+ end
89
+
90
+ def timestamp_time(t)
91
+ Time.at( t / 1000000 )
92
+ end
93
+
94
+ def encode(obj)
95
+ Base64.urlsafe_encode64(JSON.dump(obj))
96
+ end
97
+
98
+ def decode(str)
99
+ JSON.load(Base64.urlsafe_decode64(str))
100
+ end
101
+
102
+ end
103
+
104
+ class Task < Struct.new(:queue, :id, :expires, :payload, :raw)
105
+
106
+ def initialize(*args)
107
+ super
108
+ @clock = Time
109
+ end
110
+
111
+ def finish!
112
+ self.queue.finish!(self)
113
+ end
114
+
115
+ def extend!(secs=nil)
116
+ self.queue.extend!(self, secs)
117
+ end
118
+
119
+ def clock!(_)
120
+ @clock = _; return self
121
+ end
122
+
123
+ def reset_clock!
124
+ @clock = Time; return self
125
+ end
126
+
127
+ def lease_remaining
128
+ self.expires - @clock.now
129
+ end
130
+
131
+ def lease_expired?
132
+ self.expires < @clock.now
133
+ end
134
+
135
+ end
136
+
137
+ end
138
+
data/lib/tq/shell.rb ADDED
@@ -0,0 +1,85 @@
1
+ require 'optparse'
2
+ require 'json'
3
+ require_relative '../version'
4
+
5
+ module TQ
6
+
7
+ class Shell
8
+
9
+ DEFAULT_OPTIONS = {
10
+ app: {},
11
+ auth_secrets_file: './tq-client.json',
12
+ auth_store_file: './tq-client-store.json',
13
+ config_file: './tq-app.json'
14
+ }
15
+
16
+ def initialize(app, logger=nil)
17
+ @app = app
18
+ @logger = logger
19
+ @summary = []
20
+ end
21
+
22
+ def banner(_)
23
+ @banner = _; return self
24
+ end
25
+
26
+ def summary(*_)
27
+ @summary = _; return self
28
+ end
29
+
30
+ def call(argv=ARGV)
31
+
32
+ progname = File.basename(__FILE__,'.rb')
33
+
34
+ @logger.info(progname) { "Configuring #{@app.id}" } if @logger
35
+ opts = parse_args(argv)
36
+ @logger.debug(progname) { "Configuration: #{opts[:app].inspect}" } if @logger
37
+
38
+ @app = @app.options( opts[:app] ).logger(@logger)
39
+
40
+ @logger.info(progname) { "Running #{@app.id} using worker #{@app.worker}" } if @logger
41
+ @app.run!( opts[:auth_secrets_file], opts[:auth_store_file] )
42
+
43
+ end
44
+
45
+ private
46
+
47
+ def parse_args(argv)
48
+ opts = {}.merge(DEFAULT_OPTIONS)
49
+
50
+ OptionParser.new do |shell|
51
+
52
+ (shell.banner = @banner) if @banner
53
+ @summary.each do |line|
54
+ shell.separator line
55
+ end
56
+
57
+ shell.on('-a', '--auth-secrets [FILE]', "Google OAuth2 secrets file") do |given|
58
+ opts[:auth_secrets_file] = given
59
+ end
60
+
61
+ shell.on('-s', '--auth-store [FILE]', "Google OAuth2 storage file") do |given|
62
+ opts[:auth_store_file] = given
63
+ end
64
+
65
+ shell.on('-c', '--config [FILE]', "Application config file (json)") do |given|
66
+ opts[:config_file] = given
67
+ opts[:app] = JSON.load( File.open(given, 'r') )
68
+ end
69
+
70
+ shell.on('-h', '--help', "Prints this help") do |given|
71
+ puts shell; exit
72
+ end
73
+
74
+ shell.on('-v', '--version', "Prints TQ version") do |given|
75
+ puts TQ::VERSION; exit
76
+ end
77
+
78
+ end.parse(argv)
79
+
80
+ return opts
81
+ end
82
+
83
+ end
84
+
85
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,4 @@
1
+
2
+ module TQ
3
+ VERSION = '0.1.3'
4
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,120 @@
1
+ require 'fileutils'
2
+ require 'logger'
3
+
4
+ gem 'minitest'
5
+ require 'minitest/autorun'
6
+
7
+ require 'google/api_client'
8
+
9
+ module TestUtils
10
+ extend self
11
+
12
+ def setup_logger(name="test")
13
+ file = File.expand_path("log/#{name}.log", File.dirname(__FILE__))
14
+ FileUtils.mkdir_p( File.dirname(file) )
15
+ logger = Logger.new( File.open(file, 'w' ) )
16
+ logger.level = ENV['DEBUG'] ? Logger::DEBUG : Logger::INFO
17
+ Google::APIClient.logger = logger
18
+ end
19
+
20
+ def current_logger
21
+ Google::APIClient.logger
22
+ end
23
+
24
+ class QueueHelper
25
+
26
+ attr_reader :project, :queue
27
+ def initialize(project,queue)
28
+ @project, @queue = project, queue
29
+ end
30
+
31
+ def auth_files(secrets,creds)
32
+ @secrets_file, @creds_file = secrets, creds
33
+ return self
34
+ end
35
+
36
+ def authorized_client
37
+ # @authorized_client ||= TQ::App.new('test_app',nil).service_auth!(File.read(SERVICE_ISSUER_FILE).chomp, SERVICE_P12_FILE)
38
+ @authorized_client ||= TQ::App.new('test_app',nil).auth!(@secrets_file, @creds_file)
39
+ end
40
+
41
+ # Note: inaccurate, don't use
42
+ def peek()
43
+ client, api = authorized_client
44
+ results = client.execute!( :api_method => api.tasks.list,
45
+ :parameters => { :project => project, :taskqueue => queue }
46
+ )
47
+ items = results.data['items'] || []
48
+ end
49
+
50
+ def push!(payload)
51
+ client, api = authorized_client
52
+ client.execute!( :api_method => api.tasks.insert,
53
+ :parameters => { :project => project, :taskqueue => queue },
54
+ :body_object => {
55
+ 'queueName' => queue,
56
+ 'payloadBase64' => encode(payload)
57
+ }
58
+ )
59
+ end
60
+
61
+ def pop!(n=1)
62
+ client, api = authorized_client
63
+ results = client.execute!( :api_method => api.tasks.lease,
64
+ :parameters => { :project => project, :taskqueue => queue,
65
+ :leaseSecs => 60, :numTasks => n
66
+ }
67
+ )
68
+ items = results.data['items'] || []
69
+ items.each do |item|
70
+ client.execute!( :api_method => api.tasks.delete,
71
+ :parameters => { :project => project, :taskqueue => queue, :task => item['id'] }
72
+ )
73
+ end
74
+ return items
75
+ end
76
+
77
+ def all_payloads!
78
+ map! { |t| decode(t['payloadBase64']) }
79
+ end
80
+
81
+ def all_tags!
82
+ map! { |t| t['tag'] }
83
+ end
84
+
85
+ def all_payloads_and_tags!
86
+ map! { |t| {:payload => decode(t['payloadBase64']),
87
+ :tag => t['tag']
88
+ }
89
+ }
90
+ end
91
+
92
+ def map!(&b)
93
+ clear!.map(&b)
94
+ end
95
+
96
+ def clear!
97
+ client, api = authorized_client
98
+ done = false
99
+ all = []
100
+ while !done do
101
+ batch = pop!(10)
102
+ done = batch.empty? || batch.length < 10
103
+ all = all + batch
104
+ end
105
+ all
106
+ end
107
+
108
+ def encode(obj)
109
+ Base64.urlsafe_encode64(JSON.dump(obj))
110
+ end
111
+
112
+ def decode(str)
113
+ JSON.load(Base64.urlsafe_decode64(str))
114
+ end
115
+
116
+ end
117
+
118
+
119
+ end
120
+
data/test/suite.rb ADDED
@@ -0,0 +1,4 @@
1
+
2
+ %w[ test_shell test_run test_logger ].each do |rb|
3
+ require_relative rb
4
+ end
data/test/test_auth.rb ADDED
@@ -0,0 +1,63 @@
1
+ require 'fileutils'
2
+ require_relative './helper'
3
+ require_relative '../lib/tq/app'
4
+
5
+ def setup_logger!(name)
6
+ TestUtils.setup_logger(name)
7
+ end
8
+
9
+ def delete_credentials!
10
+ FileUtils.rm_f(CREDENTIALS_FILE)
11
+ end
12
+
13
+ # for installed app auth
14
+ CLIENT_SECRETS_FILE = File.expand_path('../config/secrets/test/client_secrets.json', File.dirname(__FILE__))
15
+ CREDENTIALS_FILE = File.expand_path("../config/secrets/test/#{File.basename(__FILE__,'.rb')}-oauth2.json", File.dirname(__FILE__))
16
+
17
+ # for service account auth -- not quite working
18
+ SERVICE_ISSUER_FILE = File.expand_path('../config/secrets/test/issuer', File.dirname(__FILE__))
19
+ SERVICE_P12_FILE = File.expand_path('../config/secrets/test/client.p12', File.dirname(__FILE__))
20
+
21
+ describe TQ::App do
22
+
23
+ describe "auth" do
24
+
25
+ before do
26
+ setup_logger!('auth')
27
+ delete_credentials!
28
+ end
29
+
30
+ it "should authorize without cached credentials" do
31
+ app = TQ::App.new('test_app/0.0.0', nil)
32
+ app.auth! CLIENT_SECRETS_FILE
33
+ assert true
34
+ end
35
+
36
+ # Note: browser window should only appear once for this test
37
+ # Not sure if it's possible to assert this
38
+ it "should authorize with cached credentials not existing and existing" do
39
+ app = TQ::App.new('test_app/0.0.0', nil)
40
+ app.auth! CLIENT_SECRETS_FILE, CREDENTIALS_FILE
41
+ assert File.exists?(CREDENTIALS_FILE)
42
+ app.auth! CLIENT_SECRETS_FILE, CREDENTIALS_FILE
43
+ assert File.exists?(CREDENTIALS_FILE)
44
+ end
45
+
46
+ end
47
+
48
+ describe "service auth" do
49
+
50
+ before do
51
+ setup_logger!('auth')
52
+ end
53
+
54
+ it "should authorize service account" do
55
+ app = TQ::App.new('test_app/0.0.0', nil)
56
+ app.service_auth!(File.read(SERVICE_ISSUER_FILE).chomp, SERVICE_P12_FILE)
57
+ assert true
58
+ end
59
+
60
+ end
61
+
62
+ end
63
+
@@ -0,0 +1,184 @@
1
+ # Functional tests of TQ::Logger
2
+ # Please note that these require a deployed GAE app
3
+ # with two queues: 'test' and 'log'.
4
+ # The GAE project_id is defined in ../config/secrets/test/project_id,
5
+ # along with other secrets files (see below).
6
+
7
+ require 'logger'
8
+
9
+ require_relative './helper'
10
+ require_relative '../lib/tq/app'
11
+ require_relative '../lib/tq/queue'
12
+ require_relative '../lib/tq/logger'
13
+
14
+ def setup_test_logger!
15
+ TestUtils.setup_logger( File.basename(__FILE__,'.rb') )
16
+ end
17
+
18
+
19
+ class LoggerTests < Minitest::Spec
20
+
21
+ # for installed app auth
22
+ CLIENT_SECRETS_FILE = File.expand_path(
23
+ '../config/secrets/test/client_secrets.json', File.dirname(__FILE__)
24
+ )
25
+ CREDENTIALS_FILE = File.expand_path(
26
+ "../config/secrets/test/#{File.basename(__FILE__,'.rb')}-oauth2.json",
27
+ File.dirname(__FILE__)
28
+ )
29
+
30
+ # task queue constants
31
+ TASKQUEUE_PROJECT_ID = File.read(
32
+ File.expand_path('../config/secrets/test/project_id', File.dirname(__FILE__))
33
+ ).chomp
34
+
35
+ TASKQUEUE_TEST_QUEUE = 'log'
36
+
37
+ def test_logger
38
+ @test_logger ||= TestUtils.current_logger
39
+ end
40
+
41
+ def assert_logged( exps, acts )
42
+ errs = []
43
+ unless (a = exps.length) == (b = acts.length)
44
+ errs.push( "Expected #{a} messages, #{b < 2 ? 'were' : 'was'} #{b}" )
45
+ end
46
+ exps.each_with_index do |exp,i|
47
+ act = acts[i]
48
+ next if act.nil?
49
+ [:level, :message, :progname].each do |elem|
50
+ unless (a = exp[elem]) == (b = act[elem.to_s])
51
+ errs.push( "[#{i}] Expected #{elem} to be #{a}, was #{b}" )
52
+ end
53
+ end
54
+ unless (a = ::Logger::SEV_LABEL[exp[:level]]) == (b = act['label'])
55
+ errs.push( "[#{i}] Expected label to be #{a}, was #{b}" )
56
+ end
57
+ end
58
+ assert errs.length == 0, errs.join("\n")
59
+ end
60
+
61
+ def assert_logged_tags( exps, acts )
62
+ errs = []
63
+ unless (a = exps.length) == (b = acts.length)
64
+ errs.push( "Expected #{a} messages, #{b < 2 ? 'were' : 'was'} #{b}" )
65
+ end
66
+ exps.each_with_index do |exp,i|
67
+ act = acts[i]
68
+ next if act.nil?
69
+ unless (a = exp[:method].to_s) == (b = act)
70
+ errs.push( "[#{i}] Expected tag to be #{a}, was #{b}" )
71
+ end
72
+ end
73
+ assert errs.length == 0, errs.join("\n")
74
+ end
75
+
76
+ def send_messages_to!(logger, msgs)
77
+ msgs.each do |msg|
78
+ logger.__send__(msg[:method], msg[:progname], msg[:context]) { msg[:message] }
79
+ end
80
+ end
81
+
82
+ def queue_helper(project,queue)
83
+ TestUtils::QueueHelper.new(project,queue).auth_files(CLIENT_SECRETS_FILE, CREDENTIALS_FILE)
84
+ end
85
+
86
+ def clear_queue!(project,queue)
87
+ queue_helper(project,queue).clear!
88
+ end
89
+
90
+ def verify_logged_messages_to_level!(expected_messages, minlevel)
91
+ actual_messages = queue_helper(TASKQUEUE_PROJECT_ID, TASKQUEUE_TEST_QUEUE).all_payloads_and_tags!
92
+ $stderr.puts actual_messages.inspect
93
+
94
+ selected_messages = expected_messages.select { |msg| msg[:level] >= minlevel }
95
+
96
+ assert_logged( selected_messages,
97
+ actual_messages.map { |msg| msg[:payload] }
98
+ )
99
+
100
+ assert_logged_tags( selected_messages,
101
+ actual_messages.map { |msg| msg[:tag] }
102
+ )
103
+ end
104
+
105
+ # Note: ideally these tests would also verify the (file) logger output as well
106
+ # and the timestamps, mocking the clock, etc. But that's a mess of work, so for now
107
+ # you should just eyeball it in the $stderr output.
108
+
109
+ def setup
110
+ clear_queue!(TASKQUEUE_PROJECT_ID, TASKQUEUE_TEST_QUEUE)
111
+
112
+ app = TQ::App.new('test_app/0.0.0', nil)
113
+ @queue = TQ::Queue.new( *app.auth!(CLIENT_SECRETS_FILE, CREDENTIALS_FILE) )
114
+ .options({ 'project' => TASKQUEUE_PROJECT_ID, 'name' => TASKQUEUE_TEST_QUEUE })
115
+ end
116
+
117
+ it 'default logger should log to queue at warn level' do
118
+ subject = TQ::Logger.new(@queue)
119
+
120
+ expected_messages = [
121
+ { method: :debug, level: ::Logger::DEBUG, message: 'debug message', progname: 'prog1', context: { key: 1 } },
122
+ { method: :info, level: ::Logger::INFO, message: 'info message', progname: 'prog2', context: { key: 2 } },
123
+ { method: :warn, level: ::Logger::WARN, message: 'warn message', progname: 'prog3', context: { key: 3 } },
124
+ { method: :error, level: ::Logger::ERROR, message: 'error message', progname: 'prog4', context: { key: 4 } }
125
+ ]
126
+
127
+ send_messages_to! subject, expected_messages
128
+
129
+ verify_logged_messages_to_level! expected_messages, ::Logger::WARN
130
+
131
+ end
132
+
133
+ it 'after setting level to debug, logger should log to queue at debug level' do
134
+ subject = TQ::Logger.new(@queue)
135
+ subject.level = ::Logger::DEBUG
136
+
137
+ expected_messages = [
138
+ { method: :debug, level: ::Logger::DEBUG, message: 'debug message', progname: 'prog1', context: { key: 1 } },
139
+ { method: :info, level: ::Logger::INFO, message: 'info message', progname: 'prog2', context: { key: 2 } },
140
+ { method: :warn, level: ::Logger::WARN, message: 'warn message', progname: 'prog3', context: { key: 3 } },
141
+ { method: :error, level: ::Logger::ERROR, message: 'error message', progname: 'prog4', context: { key: 4 } }
142
+ ]
143
+
144
+ send_messages_to!(subject, expected_messages)
145
+
146
+ verify_logged_messages_to_level! expected_messages, ::Logger::DEBUG
147
+
148
+ end
149
+
150
+ it 'when setting level to debug in config, logger should log to queue at debug level' do
151
+ subject = TQ::Logger.new(@queue, {'level' => ::Logger::DEBUG } )
152
+
153
+ expected_messages = [
154
+ { method: :debug, level: ::Logger::DEBUG, message: 'debug message', progname: 'prog1', context: { key: 1 } },
155
+ { method: :info, level: ::Logger::INFO, message: 'info message', progname: 'prog2', context: { key: 2 } },
156
+ { method: :warn, level: ::Logger::WARN, message: 'warn message', progname: 'prog3', context: { key: 3 } },
157
+ { method: :error, level: ::Logger::ERROR, message: 'error message', progname: 'prog4', context: { key: 4 } }
158
+ ]
159
+
160
+ send_messages_to!(subject, expected_messages)
161
+
162
+ verify_logged_messages_to_level! expected_messages, ::Logger::DEBUG
163
+
164
+ end
165
+
166
+ it 'when setting external logger, logger should log to queue at logger\'s level' do
167
+ subject = TQ::Logger.new(@queue, test_logger )
168
+
169
+ expected_messages = [
170
+ { method: :debug, level: ::Logger::DEBUG, message: 'debug message', progname: 'prog1', context: { key: 1 } },
171
+ { method: :info, level: ::Logger::INFO, message: 'info message', progname: 'prog2', context: { key: 2 } },
172
+ { method: :warn, level: ::Logger::WARN, message: 'warn message', progname: 'prog3', context: { key: 3 } },
173
+ { method: :error, level: ::Logger::ERROR, message: 'error message', progname: 'prog4', context: { key: 4 } }
174
+ ]
175
+
176
+ send_messages_to!(subject, expected_messages)
177
+
178
+ verify_logged_messages_to_level! expected_messages, test_logger.level
179
+
180
+ end
181
+
182
+ end
183
+
184
+ setup_test_logger!
data/test/test_run.rb ADDED
@@ -0,0 +1,239 @@
1
+ # Functional tests of TQ::App#run!
2
+ # Please note that these require a deployed GAE app
3
+ # with two queues: 'test' and 'log'.
4
+ # The GAE project_id is defined in ../config/secrets/test/project_id,
5
+ # along with other secrets files (see below).
6
+
7
+ require_relative './helper'
8
+ require_relative '../lib/tq/app'
9
+
10
+ def setup_test_logger!
11
+ TestUtils.setup_logger( File.basename(__FILE__,'.rb') )
12
+ end
13
+
14
+ class AppRunTests < Minitest::Spec
15
+
16
+ # for installed app auth
17
+ CLIENT_SECRETS_FILE = File.expand_path(
18
+ '../config/secrets/test/client_secrets.json', File.dirname(__FILE__)
19
+ )
20
+ CREDENTIALS_FILE = File.expand_path(
21
+ "../config/secrets/test/#{File.basename(__FILE__,'.rb')}-oauth2.json",
22
+ File.dirname(__FILE__)
23
+ )
24
+
25
+ # for service account auth -- not quite working
26
+ SERVICE_ISSUER_FILE = File.expand_path(
27
+ '../config/secrets/test/issuer', File.dirname(__FILE__)
28
+ )
29
+ SERVICE_P12_FILE = File.expand_path(
30
+ '../config/secrets/test/client.p12', File.dirname(__FILE__)
31
+ )
32
+
33
+ # task queue constants
34
+ TASKQUEUE_PROJECT_ID = File.read(
35
+ File.expand_path('../config/secrets/test/project_id', File.dirname(__FILE__))
36
+ ).chomp
37
+
38
+ TASKQUEUE_LEASE_SECS = 2
39
+
40
+
41
+ def queue_helper(project,queue)
42
+ TestUtils::QueueHelper.new(project,queue).auth_files(CLIENT_SECRETS_FILE, CREDENTIALS_FILE)
43
+ end
44
+
45
+ def tasks_on_queue(project,queue)
46
+ queue_helper(project,queue).list
47
+ end
48
+
49
+ def clear_queue!(project,queue)
50
+ queue_helper(project,queue).clear!
51
+ end
52
+
53
+ def push_tasks!(project,queue,tasks)
54
+ q = queue_helper(project,queue)
55
+ tasks.each do |task| q.push!(task) end
56
+ end
57
+
58
+
59
+ describe "run!" do
60
+
61
+ before do
62
+ sleep TASKQUEUE_LEASE_SECS + 1 # to wait for lease expiry from previous test
63
+ clear_queue!(TASKQUEUE_PROJECT_ID,'test')
64
+ clear_queue!(TASKQUEUE_PROJECT_ID,'log')
65
+ end
66
+
67
+ it "should setup clearing the queue" do
68
+ cleared = clear_queue!(TASKQUEUE_PROJECT_ID,'test')
69
+ assert_equal 0, n = cleared.length,
70
+ "Expected no tasks on queue, #{n < 2 ? 'was' : 'were'} #{n}"
71
+
72
+ # unfortunately, queue peeks are horribly inaccurate right now.
73
+ # assert_equal 0, n = tasks_on_queue(TASKQUEUE_PROJECT_ID,'test').length,
74
+ # "Expected no tasks on queue, #{n < 2 ? 'was' : 'were'} #{n}"
75
+ end
76
+
77
+ it "worker should receive input queue and :call with each task on the queue up to the specified number" do
78
+
79
+ # setup
80
+ expected_tasks = [
81
+ { 'What is your name?' => 'Sir Lancelot', 'What is your quest?' => 'To seek the holy grail', 'What is your favorite color?' => 'blue' },
82
+ { 'What is your name?' => 'Sir Robin', 'What is your quest?' => 'To seek the holy grail', 'What is the capital of Assyria?' => nil },
83
+ { 'What is your name?' => 'Galahad', 'What is your quest?' => 'To seek the grail', 'What is your favorite color?' => ['blue','yellow'] },
84
+ { 'What is your name?' => 'Arthur', 'What is your quest?' => 'To seek the holy grail', 'What is the air-speed velocity of an unladen swallow?' => 'African or European swallow?' }
85
+ ]
86
+ push_tasks!(TASKQUEUE_PROJECT_ID,'test', expected_tasks)
87
+
88
+ # expectations
89
+ mock_handler_class = MiniTest::Mock.new
90
+ mock_handler = MiniTest::Mock.new
91
+
92
+ ## expect constructor receives task queue as first param -- for each queued task up to expected_instances
93
+ expected_calls = 3
94
+ (0...expected_calls).each do
95
+ mock_handler_class.expect(:new, mock_handler) do |*args|
96
+ args.first.respond_to?('finish!')
97
+ end
98
+ end
99
+
100
+ ## expect :call for each queued task up to expected_calls
101
+ actual_calls = 0;
102
+ (0...expected_calls).each do
103
+ mock_handler.expect(:call, true) do |actual_task|
104
+ actual_calls += 1
105
+ valid = !!actual_task && actual_task.payload.has_key?('What is your name?')
106
+ valid.tap do |yes|
107
+ $stderr.puts("mock_handler.call - received: #{ actual_task.payload }") if yes
108
+ end
109
+ end
110
+ end
111
+
112
+ # execution
113
+ app = TQ::App.new('test_app/0.0.0', mock_handler_class)
114
+ .project(TASKQUEUE_PROJECT_ID)
115
+ .stdin({ 'name' => 'test', 'num_tasks' => expected_calls, 'lease_secs' => TASKQUEUE_LEASE_SECS })
116
+ app.run! CLIENT_SECRETS_FILE, CREDENTIALS_FILE
117
+
118
+ # assertions
119
+ assert_equal expected_calls, actual_calls,
120
+ "Expected #{expected_calls} worker calls, was #{actual_calls}"
121
+
122
+ mock_handler_class.verify
123
+ mock_handler.verify
124
+
125
+ end
126
+
127
+ it 'should put task back on input queue after lease_secs, if not finished' do
128
+
129
+ # setup
130
+ expected_tasks = [
131
+ { 'What is your name?' => 'Sir Lancelot', 'What is your quest?' => 'To seek the holy grail', 'What is your favorite color?' => 'blue' }
132
+ ]
133
+ push_tasks!(TASKQUEUE_PROJECT_ID,'test', expected_tasks)
134
+
135
+ class DoNothingWorker
136
+ def initialize(*args); end
137
+ def call(task); end
138
+ end
139
+
140
+ # execution
141
+ app = TQ::App.new('test_app/0.0.0', DoNothingWorker)
142
+ .project(TASKQUEUE_PROJECT_ID)
143
+ .stdin({ 'name' => 'test', 'num_tasks' => 1, 'lease_secs' => TASKQUEUE_LEASE_SECS })
144
+ app.run! CLIENT_SECRETS_FILE, CREDENTIALS_FILE
145
+
146
+ sleep TASKQUEUE_LEASE_SECS + 1
147
+ actual_tasks = clear_queue!(TASKQUEUE_PROJECT_ID,'test')
148
+
149
+ ## queue peeks are inaccurate right now...
150
+ # actual_tasks = tasks_on_queue(TASKQUEUE_PROJECT_ID,'test')
151
+
152
+ ## assertion
153
+ assert_equal a = expected_tasks.length, b = actual_tasks.length,
154
+ "Expected #{a} #{a == 1 ? 'task' : 'tasks'} on queue, #{b < 2 ? 'was' : 'were'} #{b}"
155
+
156
+ assert (n = actual_tasks.first['retry_count']) > 0,
157
+ "Expected >0 lease retry count, #{n < 2 ? 'was' : 'were'} #{n}"
158
+
159
+ end
160
+
161
+ it 'should be able to push to queue from within worker' do
162
+
163
+ # setup
164
+ expected_tasks = [
165
+ { 'What is your name?' => 'Sir Lancelot', 'What is your quest?' => 'To seek the holy grail', 'What is your favorite color?' => 'blue' }
166
+ ]
167
+ push_tasks!(TASKQUEUE_PROJECT_ID,'test', expected_tasks)
168
+
169
+ class RelayWorker
170
+ def initialize(*args)
171
+ @stdin = args.first; @stdout = args[1]
172
+ end
173
+
174
+ def call(task)
175
+ @stdout.push!(task.payload)
176
+ task.finish!
177
+ end
178
+ end
179
+
180
+ # execution
181
+ app = TQ::App.new('test_app/0.0.0', RelayWorker)
182
+ .project(TASKQUEUE_PROJECT_ID)
183
+ .stdin({ 'name' => 'test', 'num_tasks' => 1, 'lease_secs' => TASKQUEUE_LEASE_SECS })
184
+ .stdout({ 'name' => 'log' })
185
+ app.run! CLIENT_SECRETS_FILE, CREDENTIALS_FILE
186
+
187
+ sleep TASKQUEUE_LEASE_SECS + 1
188
+ actual_output_tasks = clear_queue!(TASKQUEUE_PROJECT_ID,'log')
189
+ actual_input_tasks = clear_queue!(TASKQUEUE_PROJECT_ID,'test')
190
+
191
+ # assertion
192
+
193
+ assert_equal 0, b = actual_input_tasks.length,
194
+ "Expected no tasks on input queue, #{b < 2 ? 'was' : 'were'} #{b}"
195
+
196
+ assert_equal a = expected_tasks.length, b = actual_output_tasks.length,
197
+ "Expected #{a} #{a == 1 ? 'task' : 'tasks'} on output queue, #{b < 2 ? 'was' : 'were'} #{b}"
198
+
199
+ end
200
+
201
+ it 'should extend a task lease if extended before lease expires' do
202
+
203
+ refute true, "Extending task leases does not currently work"
204
+
205
+ # setup
206
+ expected_tasks = [
207
+ { 'What is your name?' => 'Sir Lancelot', 'What is your quest?' => 'To seek the holy grail', 'What is your favorite color?' => 'blue' }
208
+ ]
209
+ push_tasks!(TASKQUEUE_PROJECT_ID,'test', expected_tasks)
210
+
211
+ class ExtendWorker
212
+ def initialize(*args)
213
+ @stdin = args.first
214
+ end
215
+
216
+ def call(task)
217
+ $stderr.puts "ExtendWorker - task #{ task.raw.inspect }"
218
+ ttl = task.lease_remaining
219
+ # sleep( ttl - 0.5 )
220
+ task = @stdin.extend!(task)
221
+ ttl2 = task.lease_remaining
222
+ $stderr.puts "ExtendWorker - ttl before extend: #{ttl}"
223
+ $stderr.puts "ExtendWorker - ttl after extend: #{ttl2}"
224
+ end
225
+ end
226
+
227
+ # execution
228
+ app = TQ::App.new('test_app/0.0.0', ExtendWorker)
229
+ .project(TASKQUEUE_PROJECT_ID)
230
+ .stdin({ 'name' => 'test', 'num_tasks' => 1, 'lease_secs' => TASKQUEUE_LEASE_SECS + 2 })
231
+ app.run! CLIENT_SECRETS_FILE, CREDENTIALS_FILE
232
+
233
+ end
234
+
235
+ end
236
+
237
+ end
238
+
239
+ setup_test_logger!
@@ -0,0 +1,128 @@
1
+ require_relative './helper'
2
+ require_relative '../lib/tq/app'
3
+ require_relative '../lib/tq/shell'
4
+
5
+ def setup_test_logger!
6
+ TestUtils.setup_logger( File.basename(__FILE__,'.rb') )
7
+ end
8
+
9
+ class ShellTests < Minitest::Spec
10
+
11
+ include
12
+ # for installed app auth
13
+ CLIENT_SECRETS_FILE = File.expand_path(
14
+ '../config/secrets/test/client_secrets.json', File.dirname(__FILE__)
15
+ )
16
+ CREDENTIALS_FILE = File.expand_path(
17
+ "../config/secrets/test/#{File.basename(__FILE__,'.rb')}-oauth2.json",
18
+ File.dirname(__FILE__)
19
+ )
20
+
21
+ # task queue constants
22
+ TASKQUEUE_APP_CONFIG =
23
+ File.expand_path("../config/secrets/test/#{File.basename(__FILE__,'.rb')}" +
24
+ "-config.json", File.dirname(__FILE__))
25
+
26
+ TASKQUEUE_LEASE_SECS = 2
27
+
28
+ class EchoWorker
29
+
30
+ def initialize(stdin, stdout, stderr, env)
31
+ @stdin = stdin
32
+ @env = env
33
+ @logger = env['logger']
34
+ end
35
+
36
+ def call(task)
37
+ @logger.info("EchoWorker") { "Received task #{task.id}" }
38
+ @logger.debug("EchoWorker") { "Task payload: #{task.payload.inspect}" }
39
+ @logger.debug("EchoWorker") { "Env: #{@env.inspect}" }
40
+ task.finish!
41
+ end
42
+
43
+ end
44
+
45
+ def logger
46
+ @logger ||= TestUtils.current_logger
47
+ end
48
+
49
+ def queue_helper(project,queue)
50
+ TestUtils::QueueHelper.new(project,queue).auth_files(CLIENT_SECRETS_FILE, CREDENTIALS_FILE)
51
+ end
52
+
53
+ def app_config
54
+ @app_config ||= JSON.load( File.open(TASKQUEUE_APP_CONFIG) )
55
+ end
56
+
57
+ def app_project_id
58
+ app_config['project']
59
+ end
60
+
61
+ def app_stdin_name
62
+ app_config['stdin']['name']
63
+ end
64
+
65
+ def app_stdin_num_tasks
66
+ app_config['stdin']['num_tasks']
67
+ end
68
+
69
+ def populate_queue!(tasks)
70
+ q = queue_helper(app_project_id, app_stdin_name)
71
+ tasks.each do |task| q.push!(task) end
72
+ end
73
+
74
+ def clear_queue!
75
+ queue_helper(app_project_id, app_stdin_name).clear!
76
+ end
77
+
78
+ def tasks_on_queue
79
+ clear_queue!
80
+ end
81
+
82
+ def assert_tasks_on_queue(exp)
83
+ assert_equal exp, n = tasks_on_queue.length,
84
+ "Expected #{exp} tasks on input queue, was #{n}"
85
+ end
86
+
87
+ def shell_args
88
+ [ "--auth-secrets", CLIENT_SECRETS_FILE,
89
+ "--auth-store", CREDENTIALS_FILE,
90
+ "--config", TASKQUEUE_APP_CONFIG
91
+ ]
92
+ end
93
+
94
+ def setup
95
+ sleep TASKQUEUE_LEASE_SECS+1
96
+ clear_queue!
97
+ @app = TQ::App.new('test_app_shell/0.0.0', EchoWorker)
98
+ end
99
+
100
+ it 'should execute and complete app-specified number of tasks on input queue' do
101
+ exps = [
102
+ { 'What is your name?' => 'Sir Lancelot',
103
+ 'What is your quest?' => 'To seek the holy grail',
104
+ 'What is your favorite color?' => 'blue' },
105
+ { 'What is your name?' => 'Sir Robin',
106
+ 'What is your quest?' => 'To seek the holy grail',
107
+ 'What is the capital of Assyria?' => nil },
108
+ { 'What is your name?' => 'Galahad',
109
+ 'What is your quest?' => 'To seek the grail',
110
+ 'What is your favorite color?' => ['blue','yellow'] },
111
+ { 'What is your name?' => 'Arthur',
112
+ 'What is your quest?' => 'To seek the holy grail',
113
+ 'What is the air-speed velocity of an unladen swallow?' => 'African or European swallow?' }
114
+ ]
115
+
116
+ populate_queue! exps
117
+
118
+ TQ::Shell.new(@app, logger).call( shell_args )
119
+
120
+ assert_tasks_on_queue(exps.length - app_stdin_num_tasks)
121
+
122
+ end
123
+
124
+ end
125
+
126
+
127
+ setup_test_logger!
128
+
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tq
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Eric Gjertsen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: google-api-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.8.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.8.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 5.5.1
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 5.5.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: parallel
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 1.4.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 1.4.1
55
+ description: Provides a simple framework for writing task worker processes
56
+ email: ericgj72@gmail.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - ".gems"
62
+ - lib/tq.rb
63
+ - lib/tq/app.rb
64
+ - lib/tq/logger.rb
65
+ - lib/tq/queue.rb
66
+ - lib/tq/shell.rb
67
+ - lib/version.rb
68
+ - test/helper.rb
69
+ - test/suite.rb
70
+ - test/test_auth.rb
71
+ - test/test_logger.rb
72
+ - test/test_run.rb
73
+ - test/test_shell.rb
74
+ homepage: https://github.com/ericgj/tq
75
+ licenses:
76
+ - MIT
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project:
94
+ rubygems_version: 2.4.5
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Ruby client for Google App Engine TaskQueue (REST API)
98
+ test_files: []