cloud-queues 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.md +4 -0
- data/lib/cloud-queues.rb +11 -0
- data/lib/cloud-queues/claim.rb +62 -0
- data/lib/cloud-queues/client.rb +167 -0
- data/lib/cloud-queues/message.rb +51 -0
- data/lib/cloud-queues/queue.rb +144 -0
- data/lib/cloud-queues/version.rb +3 -0
- metadata +93 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Andrew Regner
|
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 of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
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, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
data/lib/cloud-queues.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'excon'
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'uri'
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
require_relative "cloud-queues/version"
|
8
|
+
require_relative "cloud-queues/client"
|
9
|
+
require_relative "cloud-queues/queue"
|
10
|
+
require_relative "cloud-queues/message"
|
11
|
+
require_relative "cloud-queues/claim"
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module CloudQueues
|
2
|
+
class Claim
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_accessor :default_ttl
|
6
|
+
|
7
|
+
attr_reader :id
|
8
|
+
|
9
|
+
def initialize(queue, id, msgs)
|
10
|
+
@client = queue.client
|
11
|
+
@queue = queue.name
|
12
|
+
@id = id
|
13
|
+
|
14
|
+
# request the messages if we don't already have them
|
15
|
+
@messages = msgs || messages
|
16
|
+
|
17
|
+
@default_ttl = 43200 # 12 hours, server max
|
18
|
+
end
|
19
|
+
|
20
|
+
def queue
|
21
|
+
Queue.new(@client, @queue)
|
22
|
+
end
|
23
|
+
|
24
|
+
def each(&block)
|
25
|
+
@messages.each(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def [](index)
|
29
|
+
@messages[index] rescue messages[index]
|
30
|
+
end
|
31
|
+
|
32
|
+
def messages
|
33
|
+
msgs = refresh["messages"]
|
34
|
+
@messages = msgs.map { |message| Message.new(queue, message) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def update(options = {})
|
38
|
+
options = options.select { |opt| %w[ttl grace].include?(opt.to_s) }
|
39
|
+
options[:ttl] ||= @default_ttl unless options["ttl"]
|
40
|
+
@client.request(method: :patch, path: path, body: options, expects: 204) && true
|
41
|
+
end
|
42
|
+
|
43
|
+
def delete
|
44
|
+
@client.request(method: :delete, path: path, expects: 204) && true
|
45
|
+
end
|
46
|
+
alias_method :release, :delete
|
47
|
+
|
48
|
+
def path
|
49
|
+
"/queues/#{@queue}/claims/#{@id}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def age; refresh["age"]; end
|
53
|
+
def ttl; refresh["ttl"]; end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def refresh
|
58
|
+
@client.request(method: :get, path: path).body
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
module CloudQueues
|
2
|
+
class Client
|
3
|
+
|
4
|
+
attr_accessor :client_id
|
5
|
+
attr_accessor :token
|
6
|
+
attr_accessor :tenant
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
[:username, :api_key].each do |arg|
|
10
|
+
raise ArgumentError.new "#{arg} is a required argument." unless options[arg]
|
11
|
+
end if options[:token].nil? and options[:tenant].nil?
|
12
|
+
|
13
|
+
@client_id = SecureRandom.uuid
|
14
|
+
|
15
|
+
options.each_pair {|k, v| instance_variable_set("@#{k}".to_sym, v) }
|
16
|
+
authenticate!
|
17
|
+
end
|
18
|
+
|
19
|
+
def create(name)
|
20
|
+
request(method: :put, path: "/queues/#{name}", expects: 201)
|
21
|
+
Queue.new(self, name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def get(name)
|
25
|
+
request(method: :head, path: "/queues/#{name}", expects: 204)
|
26
|
+
Queue.new(self, name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def queues
|
30
|
+
response = request_all("queues", method: :get, path: "/queues", expects: [200, 204])
|
31
|
+
|
32
|
+
return [] if response.status == 204
|
33
|
+
|
34
|
+
response.body["queues"].map do |queue|
|
35
|
+
Queue.new(self, queue["name"])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def authenticate!
|
40
|
+
@client = Excon.new("https://identity.api.rackspacecloud.com")
|
41
|
+
@base_path = nil
|
42
|
+
|
43
|
+
if @token.nil?
|
44
|
+
request = {auth: {"RAX-KSKEY:apiKeyCredentials" => {username: @username, apiKey: @api_key}}}
|
45
|
+
response = request(method: :post, path: "/v2.0/tokens", body: request)
|
46
|
+
@token = response.body["access"]["token"]["id"]
|
47
|
+
else
|
48
|
+
# try the current token
|
49
|
+
request = {auth: {tenantId: @tenant, token: {id: @token}}}
|
50
|
+
response = request(method: :post, path: "/v2.0/tokens", body: request)
|
51
|
+
end
|
52
|
+
|
53
|
+
url_type = @internal ? "internalURL" : "publicURL"
|
54
|
+
queues = response.body["access"]["serviceCatalog"].select{|service| service["name"] == "cloudQueues" }
|
55
|
+
endpoints = queues[0]["endpoints"]
|
56
|
+
|
57
|
+
url = if @region.nil?
|
58
|
+
# pick the first region
|
59
|
+
# TODO when cloud queues goes GA, change this to response.body["access"]["user"]["RAX-AUTH:defaultRegion"]
|
60
|
+
endpoints[0][url_type].split('/')
|
61
|
+
else
|
62
|
+
endpoint = endpoints.select { |endpoint| endpoint["region"] == @region.to_s.upcase }
|
63
|
+
raise ArgumentError.new "Region #{@region.to_s.upcase} does not exist!" if endpoint.count == 0
|
64
|
+
endpoint[0][url_type].split('/')
|
65
|
+
end
|
66
|
+
|
67
|
+
host = url[0..2].join('/')
|
68
|
+
@base_path = "/" + url[3..-1].join('/')
|
69
|
+
@tenant = url[-1]
|
70
|
+
|
71
|
+
@client = Excon.new(host)
|
72
|
+
end
|
73
|
+
|
74
|
+
def request(options = {}, second_try = false)
|
75
|
+
if options[:body] and options[:body].class != String
|
76
|
+
options[:body] = options[:body].to_json
|
77
|
+
end
|
78
|
+
|
79
|
+
options[:path] = "#{@base_path}#{options[:path]}" unless options[:path].start_with?(@base_path)
|
80
|
+
|
81
|
+
options[:headers] ||= {}
|
82
|
+
options[:headers]["Content-Type"] = "application/json" if options[:body]
|
83
|
+
options[:headers]["Accept"] = "application/json"
|
84
|
+
options[:headers]["Client-ID"] = @client_id
|
85
|
+
options[:headers]["X-Auth-Token"] = @token if @token
|
86
|
+
options[:headers]["X-Project-ID"] = @tenant
|
87
|
+
|
88
|
+
options[:tcp_nodelay] = true if options[:tcp_nodelay].nil?
|
89
|
+
options[:expects] ||= 200
|
90
|
+
|
91
|
+
puts options if @debug
|
92
|
+
|
93
|
+
begin
|
94
|
+
response = @client.request(options)
|
95
|
+
rescue Excon::Errors::SocketError => e
|
96
|
+
raise unless e.message.include?("EOFError") or second_try
|
97
|
+
|
98
|
+
# this happens when the server closes the keep-alive socket and
|
99
|
+
# Excon doesn't realize it yet.
|
100
|
+
@client.reset
|
101
|
+
return request(options, true)
|
102
|
+
rescue Excon::Errors::Unauthorized => e
|
103
|
+
raise if second_try or @token.nil?
|
104
|
+
|
105
|
+
# Our @token probably expired, re-auth and try again
|
106
|
+
@token = nil
|
107
|
+
authenticate!
|
108
|
+
@client.reset # for good measure
|
109
|
+
return request(options, true)
|
110
|
+
end
|
111
|
+
|
112
|
+
begin
|
113
|
+
response.body = JSON.load(response.body) if (response.get_header("Content-Type") || "").include?("application/json")
|
114
|
+
rescue
|
115
|
+
# the api seems to like to give a json content type for a "204 no content" response
|
116
|
+
end
|
117
|
+
|
118
|
+
return response
|
119
|
+
end
|
120
|
+
|
121
|
+
def request_all(collection_key, options = {})
|
122
|
+
begin
|
123
|
+
absolute_limit = options[:query][:limit]
|
124
|
+
limit = [absolute_limit, 10].min
|
125
|
+
options[:query][:limit] = limit if options[:query][:limit]
|
126
|
+
rescue
|
127
|
+
absolute_limit = Float::INFINITY
|
128
|
+
limit = 10
|
129
|
+
end
|
130
|
+
|
131
|
+
response = request(options)
|
132
|
+
|
133
|
+
if collection_key and response.status != 204
|
134
|
+
# the next href link will have the query represented in it
|
135
|
+
options.delete :query
|
136
|
+
|
137
|
+
collection = response.body[collection_key]
|
138
|
+
|
139
|
+
while response.body[collection_key].count >= limit and collection.count < absolute_limit
|
140
|
+
next_link = response.body["links"].select{|l| l["rel"] == "next" }[0]["href"]
|
141
|
+
options[:path] = set_query_from(options[:path], next_link)
|
142
|
+
|
143
|
+
response = request(options)
|
144
|
+
|
145
|
+
break if response.status == 204
|
146
|
+
collection += response.body[collection_key]
|
147
|
+
end
|
148
|
+
|
149
|
+
response.body[collection_key] = collection
|
150
|
+
end
|
151
|
+
|
152
|
+
return response
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
# I would just like to comment that this is only necessary because the API does not return the correct
|
158
|
+
# href for the next page. The tenant id (account number) is missing from between the /v1 and /queues.
|
159
|
+
def set_query_from(original, new_uri)
|
160
|
+
original = URI.parse(original)
|
161
|
+
new_query = URI.parse(new_uri).query
|
162
|
+
original.query = new_query
|
163
|
+
return original.to_s
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module CloudQueues
|
2
|
+
class Message
|
3
|
+
|
4
|
+
attr_reader :id, :body
|
5
|
+
|
6
|
+
def initialize(queue, message)
|
7
|
+
@client = queue.client
|
8
|
+
@queue = queue.name
|
9
|
+
|
10
|
+
href = URI.parse(message["href"])
|
11
|
+
@id = href.path.split('/')[-1]
|
12
|
+
@claim = href.query.match(/(^|&)claim_id=([^&]+)/)[2] rescue nil
|
13
|
+
@body = message["body"]
|
14
|
+
@age = message["age"]
|
15
|
+
@ttl = message["ttl"]
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete!
|
19
|
+
@client.request(method: :delete, path: path, expects: 204) && true
|
20
|
+
end
|
21
|
+
|
22
|
+
def queue
|
23
|
+
Queue.new(@client, @queue)
|
24
|
+
end
|
25
|
+
|
26
|
+
def claim
|
27
|
+
@claim.nil? ? nil : Claim.new(queue, @claim, nil)
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_hash
|
31
|
+
{ttl: @ttl, body: @body}
|
32
|
+
end
|
33
|
+
|
34
|
+
def age; @age; end
|
35
|
+
def ttl; @ttl; end
|
36
|
+
|
37
|
+
# beware using this path method. it could return a path with a query argument
|
38
|
+
# at the end. this is to ensure the claim_id is provided whenever operations
|
39
|
+
# against this message are performed, however it could end up causing string
|
40
|
+
# formatting problems depending on how it's used.
|
41
|
+
def path
|
42
|
+
query = @claim ? "?claim_id=#{@claim}" : ""
|
43
|
+
"/queues/#{@queue}/messages/#{@id}#{query}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def [](key)
|
47
|
+
@body[key]
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module CloudQueues
|
2
|
+
class Queue
|
3
|
+
|
4
|
+
attr_accessor :default_ttl
|
5
|
+
|
6
|
+
attr_reader :name, :client
|
7
|
+
|
8
|
+
def initialize(client, name)
|
9
|
+
@client = client
|
10
|
+
@name = name
|
11
|
+
|
12
|
+
# TODO maybe make these defaults on the client or something, this isn't going
|
13
|
+
# to always work out the way we want
|
14
|
+
@default_ttl = 1209600 # 14 days, server max
|
15
|
+
|
16
|
+
@default_claim_ttl = 43200 # 12 hours, server max
|
17
|
+
@default_claim_grace = 300 # 5 minutes, arbitrary
|
18
|
+
end
|
19
|
+
|
20
|
+
def messages(options = {})
|
21
|
+
if options[:ids]
|
22
|
+
raise ArgumentError.new "Only 20 or less message IDs may be specified" if options[:ids].count > 20
|
23
|
+
allowed_query = %w[ids claim_id]
|
24
|
+
else
|
25
|
+
allowed_query = %w[marker limit echo include_claimed]
|
26
|
+
end
|
27
|
+
|
28
|
+
options = options.select { |opt| allowed_query.include?(opt.to_s) }
|
29
|
+
|
30
|
+
# Excon likes to CGI.escape values in a query hash, so this has to be handled manually
|
31
|
+
if options[:ids]
|
32
|
+
query = "?ids=#{options.delete(:ids).join(',')}"
|
33
|
+
options.each_pair do |key, value|
|
34
|
+
query << "&#{key}=#{CGI.escape(value)}"
|
35
|
+
end
|
36
|
+
options = query
|
37
|
+
end
|
38
|
+
|
39
|
+
response = @client.request_all(options.class == String ? nil : "messages",
|
40
|
+
method: :get, path: "#{path}/messages", expects: [200, 204], query: options)
|
41
|
+
return [] if response.status == 204
|
42
|
+
response.body.class == Hash ? process_messages(response.body["messages"]) : process_messages(response.body)
|
43
|
+
end
|
44
|
+
|
45
|
+
def get(id, options = {})
|
46
|
+
options = options[:claim_id] ? {claim_id: options[:claim_id]} : {}
|
47
|
+
msgs = @client.request(method: :get, path: "#{path}/messages/#{id}", query: options)
|
48
|
+
process_messages([msgs.body])[0]
|
49
|
+
end
|
50
|
+
|
51
|
+
def put(*msgs)
|
52
|
+
raise ArgumentError.new("Only 10 or less messages may be given at once") if msgs.count > 10
|
53
|
+
|
54
|
+
msgs = msgs.map do |message|
|
55
|
+
begin
|
56
|
+
if message.class == Message
|
57
|
+
message.to_hash
|
58
|
+
elsif message[:body] or message["body"]
|
59
|
+
{
|
60
|
+
ttl: message[:ttl] || message["ttl"] || @default_ttl,
|
61
|
+
body: message[:body] || message["body"],
|
62
|
+
}
|
63
|
+
else
|
64
|
+
{ ttl: @default_ttl, body: message }
|
65
|
+
end
|
66
|
+
rescue
|
67
|
+
{ ttl: @default_ttl, body: message }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# TODO this should probably do something with a body["partial"] == true response
|
72
|
+
resources = @client.request(method: :post, path: "#{path}/messages", body: msgs, expects: 201).body["resources"]
|
73
|
+
resources.map { |resource| URI.parse(resource).path.split('/')[-1] }
|
74
|
+
end
|
75
|
+
|
76
|
+
def claim(options = {})
|
77
|
+
query = options[:limit] ? {limit: options[:limit]} : {}
|
78
|
+
body = {
|
79
|
+
ttl: options[:ttl] || options["ttl"] || @default_claim_ttl,
|
80
|
+
grace: options[:grace] || options["grace"] || @default_claim_grace,
|
81
|
+
}
|
82
|
+
response = @client.request(method: :post, path: "#{path}/claims", body: body, query: query, expects: [201, 204])
|
83
|
+
return [] if response.status == 204
|
84
|
+
claim_id = URI.parse(response.get_header("Location")).path.split('/')[-1]
|
85
|
+
process_claim(claim_id, response.body)
|
86
|
+
end
|
87
|
+
|
88
|
+
def delete_messages(*ids)
|
89
|
+
query = "?ids=#{ids.join(',')}"
|
90
|
+
@client.request(method: :delete, path: "#{path}/messages", expects: 204, query: query) && true
|
91
|
+
end
|
92
|
+
|
93
|
+
def delete!
|
94
|
+
@client.request(method: :delete, path: "#{path}", expects: 204) && true
|
95
|
+
end
|
96
|
+
|
97
|
+
def [](key)
|
98
|
+
metadata[key]
|
99
|
+
end
|
100
|
+
|
101
|
+
def []=(key, value)
|
102
|
+
new_data = metadata
|
103
|
+
new_data[key] = value
|
104
|
+
@client.request(method: :put, path: "#{path}/metadata", body: new_data, expects: 204) && true
|
105
|
+
end
|
106
|
+
|
107
|
+
def metadata
|
108
|
+
@client.request(method: :get, path: "#{path}/metadata").body
|
109
|
+
end
|
110
|
+
|
111
|
+
def metadata=(new_data)
|
112
|
+
@client.request(method: :put, path: "#{path}/metadata", body: new_data, expects: 204) && true
|
113
|
+
end
|
114
|
+
|
115
|
+
def stats
|
116
|
+
@client.request(method: :get, path: "#{path}/stats").body
|
117
|
+
end
|
118
|
+
|
119
|
+
def stat(name)
|
120
|
+
stats["messages"][name.to_s]
|
121
|
+
end
|
122
|
+
|
123
|
+
def claimed; stat(:claimed); end
|
124
|
+
def free; stat(:free); end
|
125
|
+
def total; stat(:total); end
|
126
|
+
def newest; stat(:newest); end
|
127
|
+
def oldest; stat(:oldest); end
|
128
|
+
|
129
|
+
def path
|
130
|
+
"/queues/#{@name}"
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def process_messages(msgs)
|
136
|
+
msgs.map { |message| Message.new(self, message) }
|
137
|
+
end
|
138
|
+
|
139
|
+
def process_claim(claim_id, msgs)
|
140
|
+
Claim.new(self, claim_id, process_messages(msgs))
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cloud-queues
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Andrew Regner
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-10-13 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: excon
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.25.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.25.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: ! 'cloud-queues
|
47
|
+
|
48
|
+
================
|
49
|
+
|
50
|
+
|
51
|
+
Basic (unoffical) ruby interface into Rackspace Cloud Queues
|
52
|
+
|
53
|
+
'
|
54
|
+
email:
|
55
|
+
- andrew.regner@rackspace.com
|
56
|
+
executables: []
|
57
|
+
extensions: []
|
58
|
+
extra_rdoc_files: []
|
59
|
+
files:
|
60
|
+
- lib/cloud-queues/claim.rb
|
61
|
+
- lib/cloud-queues/client.rb
|
62
|
+
- lib/cloud-queues/message.rb
|
63
|
+
- lib/cloud-queues/queue.rb
|
64
|
+
- lib/cloud-queues/version.rb
|
65
|
+
- lib/cloud-queues.rb
|
66
|
+
- LICENSE
|
67
|
+
- README.md
|
68
|
+
homepage: https://github.com/adregner/cloud-queues
|
69
|
+
licenses:
|
70
|
+
- MIT
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ! '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
requirements: []
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.8.25
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Basic (unoffical) ruby interface into Rackspace Cloud Queues
|
93
|
+
test_files: []
|