cloud-queues 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.
- 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: []
|