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 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
@@ -0,0 +1,4 @@
1
+ cloud-queues
2
+ ================
3
+
4
+ Basic (unoffical) ruby interface into Rackspace Cloud Queues
@@ -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
@@ -0,0 +1,3 @@
1
+ module CloudQueues
2
+ VERSION = "1.0.0"
3
+ 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: []