SQS 0.1.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/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2006 Zachary Holt
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,3 @@
1
+ Please see the MIT-LICENSE file for copyright and distribution information.
2
+
3
+ SQS is a Ruby interface to Amazon's Simple Queue Service.
data/lib/sqs.rb ADDED
@@ -0,0 +1,294 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ dir = File::dirname( __FILE__ )
4
+
5
+ #require "#{dir}/aws"
6
+ require 'base64'
7
+ require 'erb'
8
+ require 'net/http'
9
+ require 'openssl'
10
+ require 'rexml/document'
11
+ #include Digest
12
+ include ERB::Util
13
+ include OpenSSL
14
+ include REXML
15
+
16
+
17
+ class SQS
18
+
19
+ dir = File.dirname( __FILE__ )
20
+ require "#{dir}/sqs/queue"
21
+ require "#{dir}/sqs/message"
22
+ require "#{dir}/sqs/grant"
23
+
24
+
25
+ def self.new
26
+ # rather than a singleton class, never allow instantiation
27
+ return SQS
28
+ end
29
+
30
+ def self.prepare_parameters_for_signing( hsh={} )
31
+ # Param-name1Param-value1Param-name2Param-value2...Param-nameNParam-valueN
32
+ # see http://docs.amazonwebservices.com/AWSSimpleQueueService/2006-04-01/Query/QueryAuth.html
33
+ hsh.keys.sort{ |a,b| a.to_s.casecmp( b.to_s ) }.map{ |a| "#{a.to_s}#{hsh[a].to_s}"}.join( '' )
34
+ end
35
+
36
+ def self.prepare_parameters_for_query( hsh={} )
37
+ # Param-name1Param-value1Param-name2Param-value2...Param-nameNParam-valueN
38
+ # see http://docs.amazonwebservices.com/AWSSimpleQueueService/2006-04-01/Query/QueryAuth.html
39
+ hsh.keys.map{ |a| "#{url_encode( a.to_s )}=#{url_encode( hsh[a].to_s )}"}.join( '&' )
40
+ end
41
+
42
+ def self.access_key_id=( i )
43
+ @@access_key_id = i
44
+ end
45
+ def self.access_key_id
46
+ @@access_key_id
47
+ end
48
+ def self.secret_access_key=( k )
49
+ @@secret_access_key = k
50
+ end
51
+ def self.secret_access_key
52
+ @@secret_access_key
53
+ end
54
+ def self.url_for_auery=( u )
55
+ @@url_for_query = u
56
+ end
57
+ def self.url_for_query
58
+ @@url_for_query
59
+ end
60
+
61
+ def self.create_signature( params )
62
+ prepared_parameters = self.prepare_parameters_for_signing( params )
63
+ digest = OpenSSL::HMAC::digest( OpenSSL::Digest::Digest.new("SHA1"), self.secret_access_key, prepared_parameters )
64
+ b64_hmac = Base64.encode64( digest ).strip
65
+ b64_hmac
66
+ end
67
+
68
+ def self.permissions( perm=nil )
69
+ perms = { :full => 'FULLCONTROL', :send => 'SENDMESSAGE', :receive => 'RECEIVEMESSAGE' }
70
+ if perm.nil?
71
+ perms
72
+ else
73
+ perms.has_value?( perm.to_s.upcase ) ? perm.to_s.upcase : perms[perm.to_sym]
74
+ end
75
+ end
76
+
77
+ def self.create_queue( params={} )
78
+ params = params.is_a?( Hash ) ? params : { :name => params }
79
+ params[:Action] = 'CreateQueue'
80
+ return false if params[:name].to_s.empty?
81
+ params[:QueueName] = params[:name]
82
+ params.delete( :name )
83
+
84
+ doc = self.call_web_service( params )
85
+ queue_url = doc.queue_url
86
+ SQS::Queue.new( :url => queue_url )
87
+
88
+ rescue Exception => e
89
+ raise e
90
+ end
91
+
92
+
93
+
94
+ def self.get_queue( params={} )
95
+ params = params.is_a?( Hash ) ? params : { :name => params }
96
+ self.list_queues( :prefix => params[:name] ).each do |queue|
97
+ return queue if queue.name == params[:name]
98
+ end
99
+
100
+ raise "Could not find the queue named '#{params[:name]}'"
101
+ rescue Exception => e
102
+ raise e
103
+ end
104
+
105
+
106
+ def self.list_queues( params={} )
107
+ # use :prefix => 'Boo' to get queues whose names begin with 'Boo'
108
+ params = params.is_a?( Hash ) ? params : { }
109
+ params[:Action] = 'ListQueues'
110
+ if ( params.has_key?( :prefix ) )
111
+ params[:QueueNamePrefix] = params[:prefix]
112
+ params.delete( :prefix )
113
+ end
114
+
115
+ doc = self.call_web_service( params )
116
+ queues = Array.new()
117
+
118
+ begin
119
+ XPath.each( doc, '/ListQueuesResponse/QueueUrl' ) do |element|
120
+ queues << SQS::Queue.new( :url => element.text )
121
+ end
122
+ rescue RuntimeError => e
123
+ queues = Array.new()
124
+ end
125
+ queues
126
+ rescue Exception => e
127
+ raise e
128
+ end
129
+
130
+ def self.each_queue( params={}, &block )
131
+ self.list_queues( params ).each do |q|
132
+ block.call( q )
133
+ end
134
+ rescue Exception => e
135
+ raise e
136
+ end
137
+
138
+
139
+
140
+ def self.api_version
141
+ '2006-04-01'
142
+ end
143
+
144
+ def self.signature_version
145
+ 1
146
+ end
147
+
148
+ def self.request_ttl
149
+ 30
150
+ end
151
+
152
+ def self.retry_attempts
153
+ 10
154
+ end
155
+
156
+ def self.params_for_query( t=nil )
157
+ expires = t.is_a?( Time ) ? t : Time.now.request_expires
158
+
159
+ {
160
+ :DefaultVisibilityTimeout => SQS::Queue.default_visibility_timeout,
161
+ :Version => self.api_version,
162
+ :AWSAccessKeyId => self.access_key_id,
163
+ :SignatureVersion => self.signature_version,
164
+ :Expires => expires.to_iso8601
165
+ }
166
+ end
167
+
168
+ def self.counter
169
+ @@counter
170
+ end
171
+ def self.reset_counter
172
+ @@counter = 0
173
+ end
174
+ def self.increment_counter
175
+ @@counter += 1
176
+ end
177
+ protected
178
+ @@counter = 0;
179
+ @@secret_access_key = 'Replace this with your secret access key'
180
+ @@access_key_id = 'Replace this with your access key id'
181
+ @@url_for_query = 'http://queue.amazonaws.com/'
182
+
183
+
184
+ def self.reasons_not_to_retry
185
+ [ 'Success', 'InvalidParameterValue' ]
186
+ end
187
+
188
+ def self.call_web_service( params )
189
+ params = self.params_for_query.merge( params )
190
+
191
+ begin
192
+ retries = params[:retries] + 0
193
+ params.delete( :retries )
194
+ retries = 1 if retries < 1
195
+ rescue
196
+ retries = self.retry_attempts
197
+ end
198
+
199
+ if !params[:queue].nil? && params[:queue].respond_to?( :url )
200
+ url = params[:queue].url
201
+ params.delete( :queue )
202
+ else
203
+ url = "#{self.url_for_query}"
204
+ end
205
+ url += "?#{self.prepare_parameters_for_query( params )}&Signature="
206
+
207
+ url_encoded_signature = url_encode( self.create_signature( params ) )
208
+ response = Net::HTTP.get( URI.parse( url + url_encoded_signature ) )
209
+ self.increment_counter
210
+ doc = Document.new( response )
211
+ status = doc.status
212
+
213
+ unless status == 'Success'
214
+ error_code = doc.error_code
215
+ error_message = doc.error_message
216
+ case error_code
217
+ when 'AuthFailure', 'InvalidParameterValue', 'AWS.SimpleQueueService.NonEmptyQueue'
218
+ raise( "#{error_code}: #{error_message} (#{url}#{url_encoded_signature})" )
219
+ else
220
+ raise( "Unknown Error: #{doc} (#{url}#{url_encoded_signature})" )
221
+ end
222
+ end
223
+
224
+ doc
225
+ rescue Exception => e
226
+ raise e.is_a?( SocketError ) ? "Do you have internet access? (#{e.message})" : e
227
+ end
228
+ end
229
+
230
+ class Time
231
+ def to_iso8601
232
+ self.utc.strftime( "%Y-%m-%dT%H:%M:%SZ" )
233
+ end
234
+
235
+ def request_expires
236
+ self + SQS.request_ttl
237
+ end
238
+ end
239
+
240
+ module REXML
241
+ class Document
242
+
243
+ def error_code ; self.node_text( '/Response/Errors/Error/Code' ) ; end
244
+ def error_message ; self.node_text( '/Response/Errors/Error/Message' ) ; end
245
+ def request_id ; self.node_text( '*/ResponseStatus/RequestId' ) ; end
246
+
247
+ def queue_url ; self.node_text( '/CreateQueueResponse/QueueUrl' ) ; end
248
+ def status ; self.node_text( '*/ResponseStatus/StatusCode' ) ; end
249
+
250
+ def message_id( type=:send ) ; self.node_text( "/#{type.to_s.capitalize}MessageResponse/#{ type == :send ? '' : 'Message/'}MessageId" ) ; end
251
+ def message_body( type=:peek ) ; self.node_text( "/#{type.to_s.capitalize}MessageResponse/Message/MessageBody" ) ; end
252
+
253
+ def messages( type=:receive ) ; self.nodes( "/#{type.to_s.capitalize}MessageResponse#{ type == :send ? '' : '/Message'}" ) ; end
254
+
255
+ def visibility_timeout( type=:get ) ; self.node_text( "/#{type.to_s.capitalize}VisibilityTimeoutResponse/VisibilityTimeout" ).to_i ; end
256
+
257
+ def grant_lists( type=:list ) ; self.nodes("/#{type.to_s.capitalize}GrantsResponse#{ type == :send ? '' : '/GrantList'}" ) ; end
258
+
259
+ protected
260
+ def node_text( xpath )
261
+ code = XPath.first( self, xpath )
262
+ code.to_s.empty? ? '' : code.text
263
+ end
264
+
265
+ def nodes( xpath )
266
+ XPath.match( self, xpath )
267
+ end
268
+ end
269
+
270
+
271
+ class Element
272
+ def message_id ; self.node_text( "MessageId" ) ; end
273
+ def message_body ; self.node_text( "MessageBody" ) ; end
274
+
275
+ def id( type=:message ) ; self.node_text( type == :grantee ? "ID" : "Id" ) ; end
276
+ def display_name ; self.node_text( "DisplayName" ) ; end
277
+ def permission ; self.node_text( "Permission" ) ; end
278
+
279
+ def grantees( type=:list ) ; self.nodes("Grantee" ) ; end
280
+
281
+ protected
282
+ def node_text( xpath )
283
+ code = XPath.first( self, xpath )
284
+ code.to_s.empty? ? '' : code.text
285
+ end
286
+
287
+ def nodes( xpath )
288
+ XPath.match( self, xpath )
289
+ end
290
+ end
291
+
292
+ end
293
+
294
+ Dir[File.join( File.join( File.dirname( __FILE__ ), 'sqs' ), "*.rb" )].each { |f| require f }
data/lib/sqs/grant.rb ADDED
@@ -0,0 +1,34 @@
1
+
2
+ class SQS::Grant
3
+ def self.atts
4
+ [:email, :display_name, :id, :_permission]
5
+ end
6
+
7
+ self.atts.each do |att|
8
+ attr_accessor att
9
+ end
10
+
11
+ def initialize( options={} )
12
+ if options.is_a?( Hash )
13
+ SQS::Grant.atts.each do |thing|
14
+ self.send "#{thing}=", options[thing.to_s =~ /^_(.*)$/ ? $1.to_sym : thing]
15
+ end
16
+ else
17
+ self.email = options
18
+ end
19
+ end
20
+
21
+ def permission
22
+ self._permission
23
+ end
24
+
25
+ def permission=( p )
26
+ if SQS.permissions.has_key?( p.to_s.to_sym )
27
+ self._permission = SQS.permissions[p.to_s.to_sym]
28
+ else
29
+ self._permission = p.to_s.upcase
30
+ end
31
+ end
32
+ end
33
+
34
+
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/local/ruby
2
+
3
+ class SQS::Message
4
+ attr_accessor :queue, :_body, :id
5
+
6
+ def initialize( options={} )
7
+ self.body = options.is_a?( Hash ) ? options[:body] : nil
8
+ self.queue = options.is_a?( Hash ) ? options[:queue] : nil
9
+ self.id = options.is_a?( Hash ) ? options[:id] : options.to_s
10
+ end
11
+
12
+
13
+ def delete
14
+ raise "This #{self.class.to_s} cannot be deleted, because there is no associated SQS::Queue" unless self.queue.is_a?( SQS::Queue )
15
+ raise "Cannot delete a message with no id." if self.id.to_s.empty?
16
+
17
+ doc = SQS.call_web_service( :Action => 'DeleteMessage', :queue => self.queue, :MessageId => self.id )
18
+ true
19
+ rescue Exception => e
20
+ raise e
21
+ end
22
+
23
+
24
+ def peek
25
+ raise "This #{self.class.to_s} cannot be peeked, because there is no associated SQS::Queue" unless self.queue.is_a?( SQS::Queue )
26
+ raise "Cannot peek a message with no id." if self.id.to_s.empty?
27
+
28
+ doc = SQS.call_web_service( :Action => 'PeekMessage', :queue => self.queue, :MessageId => self.id )
29
+ self.body = doc.message_body
30
+ true
31
+ rescue Exception => e
32
+ raise e
33
+ end
34
+
35
+ def body
36
+ self.peek if self._body.to_s.empty?
37
+ self._body
38
+ end
39
+ def body=( b )
40
+ self._body = b
41
+ end
42
+
43
+ end
data/lib/sqs/queue.rb ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/local/ruby
2
+
3
+ require 'uri'
4
+ require 'rexml/document'
5
+ include REXML
6
+
7
+ class SQS::Queue
8
+ private
9
+ @attributes = {}
10
+ @@acceptable_schemes = ['https', 'http']
11
+
12
+ public
13
+ attr_accessor :url
14
+ def initialize( options={} )
15
+ self.url = options.is_a?( Hash ) ? options[:url] : options
16
+ bad_url = false
17
+
18
+ begin
19
+ u = URI.parse( self.url )
20
+ bad_url = !@@acceptable_schemes.include?( u.scheme ) || u.host.to_s.empty? || u.path.to_s.empty?
21
+ rescue URI::InvalidURIError, NoMethodError
22
+ bad_url = true
23
+ end
24
+ raise "'#{self.url}' does not seem to be a parseable URL. (Please provide all parts of the URL.)" if bad_url
25
+ end
26
+
27
+ def ==( other_queue )
28
+ self.url == other_queue.url
29
+ rescue
30
+ false
31
+ end
32
+
33
+ def to_s
34
+ "#{self.class.to_s} '#{self.name}' (#{self.url})"
35
+ end
36
+
37
+ def delete
38
+ doc = SQS.call_web_service( :Action => 'DeleteQueue', :queue => self )
39
+ true
40
+ rescue Exception => e
41
+ raise e
42
+ end
43
+
44
+ def force_delete
45
+ begin
46
+ self.delete
47
+ rescue
48
+ while m = self.receive_message
49
+ m.delete if m.respond_to?( :delete )
50
+ end
51
+ populated = 2
52
+ while populated > 0
53
+ begin
54
+ self.delete
55
+ populated = 0
56
+ rescue
57
+ populated -= 1
58
+ end
59
+ end
60
+ end
61
+ true
62
+ rescue Exception => e
63
+ raise e
64
+ end
65
+
66
+
67
+ def send_message( m )
68
+ doc = SQS.call_web_service( :Action => 'SendMessage', :queue => self, :MessageBody => m.to_s )
69
+ m = SQS::Message.new( :id => doc.message_id, :queue => self )
70
+ rescue Exception => e
71
+ raise e
72
+ end
73
+
74
+ def receive_message
75
+ # returns the first element of what receive_messages returns
76
+ m = self.receive_messages( :count => 1 ).first
77
+ yield( m ) if block_given?
78
+ m
79
+ end
80
+ def receive_messages( options={} )
81
+ # returns an array of SQS::Message instances (could be empty)
82
+ count = options.has_key?( :count ) ? options[:count].to_i : 1
83
+ params = { :Action => 'ReceiveMessage', :queue => self, :NumberOfMessages => count }
84
+ params[:VisibilityTimeout] = options[:message_ttl] if options.has_key?( :message_ttl )
85
+ doc = SQS.call_web_service( params )
86
+
87
+ messages = Array.new
88
+ doc.messages.each do |m|
89
+ messages << SQS::Message.new( :id => m.message_id, :queue => self, :body => m.message_body)
90
+ end
91
+ messages
92
+ rescue Exception => e
93
+ raise e
94
+ end
95
+
96
+ def visibility_timeout=( timeout )
97
+ doc = SQS.call_web_service( :Action => 'SetVisibilityTimeout', :queue => self, :VisibilityTimeout => timeout.to_i )
98
+ true
99
+ rescue Exception => e
100
+ raise e
101
+ end
102
+ def visibility_timeout
103
+ doc = SQS.call_web_service( :Action => 'GetVisibilityTimeout', :queue => self )
104
+ doc.visibility_timeout.to_i
105
+ rescue Exception => e
106
+ raise e
107
+ end
108
+
109
+
110
+
111
+ def add_grant( options={} )
112
+ doc = SQS.call_web_service(
113
+ :Action => 'AddGrant',
114
+ :queue => self,
115
+ :Permission => SQS.permissions[options[:permission]],
116
+ 'Grantee.EmailAddress'.to_sym => options[:email].respond_to?( :email ) ? options[:email].email : options[:email]
117
+ )
118
+ true
119
+ rescue Exception => e
120
+ raise e
121
+ end
122
+
123
+ def remove_grant( options={} )
124
+ params = {
125
+ :Action => 'RemoveGrant',
126
+ :queue => self,
127
+ :Permission => SQS.permissions( options[:permission] ),
128
+ }
129
+
130
+ if !options[:id].nil?
131
+ params['Grantee.ID'.to_sym] = options[:id].respond_to?( :id ) && !options[:id].is_a?( String ) ? options[:id].id : options[:id]
132
+ elsif !options[:email].nil?
133
+ params['Grantee.EmailAddress'.to_sym] = options[:email].respond_to?( :email ) ? options[:email].email : options[:email]
134
+ else
135
+ raise "You must specify either a grant id (:id) or a grantee email address (:email) when calling remove_grant."
136
+ end
137
+
138
+ doc = SQS.call_web_service( params )
139
+ true
140
+ rescue Exception => e
141
+ raise e
142
+ end
143
+
144
+ def list_grants
145
+ doc = SQS.call_web_service( :Action => 'ListGrants', :queue => self )
146
+ grants = Array.new
147
+ doc.grant_lists.each do |list|
148
+ list.grantees.each do |g|
149
+ grants << SQS::Grant.new( :id => g.id( :grantee ), :display_name => g.display_name, :permission => list.permission )
150
+ end
151
+ end
152
+ grants
153
+ rescue Exception => e
154
+ raise e
155
+ end
156
+
157
+ def each_grant( &block )
158
+ self.list_grants.each do |g|
159
+ block.call( g )
160
+ end
161
+ end
162
+
163
+ def name
164
+ # return everything after the last slash
165
+ self.url =~ /([^\/]*)$/ ? $1 : ''
166
+ end
167
+
168
+ def self.default_visibility_timeout
169
+ 30
170
+ end
171
+
172
+ def exist?
173
+ SQS.get_queue( self.name )
174
+ true
175
+ rescue
176
+ false
177
+ end
178
+ alias_method :exists?, :exist?
179
+ end