bisques 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +70 -0
- data/Rakefile +2 -0
- data/lib/bisques/aws_connection.rb +100 -0
- data/lib/bisques/aws_credentials.rb +28 -0
- data/lib/bisques/aws_request.rb +114 -0
- data/lib/bisques/aws_request_authorization.rb +174 -0
- data/lib/bisques/aws_response.rb +36 -0
- data/lib/bisques/client.rb +148 -0
- data/lib/bisques/message.rb +47 -0
- data/lib/bisques/queue.rb +148 -0
- data/lib/bisques/queue_listener.rb +89 -0
- data/lib/bisques.rb +14 -0
- metadata +139 -0
data/README.rdoc
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
Bisques is a client library for Amazon SQS (Simple Queue Service). It is
|
2
|
+
implemented using httpclient (https://github.com/nahi/httpclient).
|
3
|
+
|
4
|
+
* http://github.com/jemmyw/bisques
|
5
|
+
|
6
|
+
== USAGE
|
7
|
+
|
8
|
+
To interact with SQS initialize a Bisques::Client object.
|
9
|
+
|
10
|
+
The following scripts will result in the producer printing 10 numbers from the
|
11
|
+
fibonacci sequence, with the consumer script doing the actual calculation.
|
12
|
+
|
13
|
+
=== Producer
|
14
|
+
|
15
|
+
require 'bisques'
|
16
|
+
|
17
|
+
Bisques::AwsCredentials.default(aws_key, aws_secret)
|
18
|
+
client = Bisques::Client.new('us-east-1')
|
19
|
+
number_queue = client.get_or_create_queue('numbers')
|
20
|
+
result_queue = client.get_or_create_queue('results')
|
21
|
+
|
22
|
+
1.upto(10).each{|n| number_queue.post_message({"number" => n}) }
|
23
|
+
|
24
|
+
10.times do
|
25
|
+
puts result_queue.retrieve_one.inspect
|
26
|
+
end
|
27
|
+
|
28
|
+
=== Consumer
|
29
|
+
|
30
|
+
require 'bisques'
|
31
|
+
|
32
|
+
Bisques::AwsCredentials.default(aws_key, aws_secret)
|
33
|
+
client = Bisques::Client.new('us-east-1')
|
34
|
+
number_queue = client.get_or_create_queue('numbers')
|
35
|
+
result_queue = client.get_or_create_queue('results')
|
36
|
+
|
37
|
+
listener = Bisques::QueueListener.new(number_queue)
|
38
|
+
listener.listen do |message|
|
39
|
+
result = fib(message["number"])
|
40
|
+
result_queue.post_message({"result" => result})
|
41
|
+
message.delete
|
42
|
+
end
|
43
|
+
|
44
|
+
while true; sleep 1; end
|
45
|
+
|
46
|
+
== LICENSE
|
47
|
+
|
48
|
+
Copyright (c) 2012 Jeremy Wells
|
49
|
+
|
50
|
+
Permission is hereby granted, free of charge, to any person
|
51
|
+
obtaining a copy of this software and associated documentation
|
52
|
+
files (the "Software"), to deal in the Software without
|
53
|
+
restriction, including without limitation the rights to use,
|
54
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
55
|
+
copies of the Software, and to permit persons to whom the
|
56
|
+
Software is furnished to do so, subject to the following
|
57
|
+
conditions:
|
58
|
+
|
59
|
+
The above copyright notice and this permission notice shall be
|
60
|
+
included in all copies or substantial portions of the Software.
|
61
|
+
|
62
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
63
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
64
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
65
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
66
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
67
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
68
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
69
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
70
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'httpclient'
|
2
|
+
require 'bisques/aws_request'
|
3
|
+
require 'bisques/aws_credentials'
|
4
|
+
|
5
|
+
module Bisques
|
6
|
+
class AwsActionError < Bisques::Error
|
7
|
+
attr_reader :type, :code, :message, :status
|
8
|
+
|
9
|
+
def initialize(type, code, message, status)
|
10
|
+
@type, @code, @message, @status = type, code, message, status
|
11
|
+
super(message)
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
"HTTP #{status}: #{type} #{code} #{message}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# This module is for making API classes more convenient. The including class
|
20
|
+
# must pass the correct params via super from it's #initialize call. Two
|
21
|
+
# useful methods are added to the including class, #request and #action.
|
22
|
+
module AwsConnection
|
23
|
+
def self.included(mod) # :nodoc:
|
24
|
+
mod.class_eval do
|
25
|
+
attr_accessor :credentials, :region, :service
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Give the region, service and optionally the AwsCredentials.
|
30
|
+
#
|
31
|
+
# === Example:
|
32
|
+
#
|
33
|
+
# class Sqs
|
34
|
+
# include AwsConnection
|
35
|
+
#
|
36
|
+
# def initialize(region)
|
37
|
+
# super(region, 'sqs')
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
def initialize(region, service, credentials = AwsCredentials.default)
|
42
|
+
@region, @service, @credentials = region, service, credentials
|
43
|
+
end
|
44
|
+
|
45
|
+
def connection # :nodoc:
|
46
|
+
@connection ||= HTTPClient.new.tap do |http|
|
47
|
+
http.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
48
|
+
http.receive_timeout = 30
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Perform an HTTP query to the given path using the given method (GET,
|
53
|
+
# POST, PUT, DELETE). A hash of query params can be specified. A POST or
|
54
|
+
# PUT body cna be specified as either a string or a hash of form params. A
|
55
|
+
# hash of HTTP headers can be specified.
|
56
|
+
def request(method, path, query = {}, body = nil, headers = {})
|
57
|
+
AwsRequest.new(connection).tap do |aws_request|
|
58
|
+
aws_request.credentials = credentials
|
59
|
+
aws_request.path = path
|
60
|
+
aws_request.region = region
|
61
|
+
aws_request.service = service
|
62
|
+
aws_request.method = method
|
63
|
+
aws_request.query = query
|
64
|
+
aws_request.body = body
|
65
|
+
aws_request.headers = headers
|
66
|
+
aws_request.make_request
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Call an AWS API with the given name at the given path. An optional hash
|
71
|
+
# of options can be passed as arguments for the API call. Returns an
|
72
|
+
# AwsResponse. If the response is not successful then an AwsActionError is
|
73
|
+
# raised and the error information is extracted into the exception
|
74
|
+
# instance.
|
75
|
+
#
|
76
|
+
# The API call will be automatically retried if the returned status code is
|
77
|
+
# in the 500 range.
|
78
|
+
def action(action_name, path = "/", options = {})
|
79
|
+
retries = 0
|
80
|
+
|
81
|
+
begin
|
82
|
+
# If options given in place of path assume /
|
83
|
+
options, path = path, "/" if path.is_a?(Hash) && options.empty?
|
84
|
+
request(:post, path, {}, options.merge("Action" => action_name)).response.tap do |response|
|
85
|
+
unless response.success?
|
86
|
+
element = response.doc.xpath("//Error")
|
87
|
+
raise AwsActionError.new(element.xpath("Type").text, element.xpath("Code").text, element.xpath("Message").text, response.http_response.status)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
rescue AwsActionError => e
|
91
|
+
if retries < 2 && (500..599).include?(e.status.to_i)
|
92
|
+
retries += 1
|
93
|
+
retry
|
94
|
+
else
|
95
|
+
raise e
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Bisques
|
2
|
+
# Represents an AWS key/secret combination. Provides a convenient class
|
3
|
+
# method for setting defaults that can be used by all objects later on.
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
#
|
7
|
+
# AwsCredentials.default('aws_key', 'aws_secret')
|
8
|
+
#
|
9
|
+
class AwsCredentials
|
10
|
+
attr_reader :aws_key, :aws_secret
|
11
|
+
|
12
|
+
def initialize(aws_key, aws_secret)
|
13
|
+
@aws_key, @aws_secret = aws_key, aws_secret
|
14
|
+
end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def default(*args)
|
18
|
+
if args.size == 2
|
19
|
+
@default = AwsCredentials.new(*args)
|
20
|
+
elsif args.empty?
|
21
|
+
@default
|
22
|
+
else
|
23
|
+
raise ArgumentError, "default takes 0 or 2 arguments"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'bisques/aws_request_authorization'
|
2
|
+
require 'bisques/aws_response'
|
3
|
+
|
4
|
+
module Bisques
|
5
|
+
# A request to an AWS API call. This class must be initiated with a client
|
6
|
+
# instance of HTTPClient. A number of mandatory attributes must be set before
|
7
|
+
# calling make_request to return the response. #make_request returns an
|
8
|
+
# AwsResponse object.
|
9
|
+
#
|
10
|
+
# === Example
|
11
|
+
#
|
12
|
+
# request = AwsRequest.new(httpclient)
|
13
|
+
# request.method = "GET" or "POST"
|
14
|
+
# request.query = {"hash" => "of query params"}
|
15
|
+
# request.body = {"hash" => "of form params"} or "text body"
|
16
|
+
# request.headers = {"hash" => "of optional headers"}
|
17
|
+
# request.path = "optional path"
|
18
|
+
# request.region = "AWS region (ex. us-east-1)"
|
19
|
+
# request.service = "AWS service (ex. sqs)"
|
20
|
+
# request.credentials = AwsCredentials.new("aws_key", "aws_secret")
|
21
|
+
# response = request.make_request => Returns AwsResponse
|
22
|
+
#
|
23
|
+
class AwsRequest
|
24
|
+
# The HTTP method to use. Should be GET or POST.
|
25
|
+
attr_accessor :method
|
26
|
+
# A hash describing the query params to send.
|
27
|
+
attr_accessor :query
|
28
|
+
# A hash or string describing the form params or HTTP body. Only used when
|
29
|
+
# the method is POST or PUT.
|
30
|
+
attr_accessor :body
|
31
|
+
# A hash of additional HTTP headers to send with the request.
|
32
|
+
attr_accessor :headers
|
33
|
+
# The path to the API call. This shouldn't be the full URL as the host part
|
34
|
+
# is built from the region and service.
|
35
|
+
attr_accessor :path
|
36
|
+
# The AWS region. Ex: us-east-1
|
37
|
+
attr_accessor :region
|
38
|
+
# The AWS service. Ex: sqs
|
39
|
+
attr_accessor :service
|
40
|
+
# The AWS credentials. Should respond to aws_key and aws_secret. Use
|
41
|
+
# AwsCredentials.
|
42
|
+
attr_accessor :credentials
|
43
|
+
|
44
|
+
# An AwsResponse object describing the response. Returns nil until
|
45
|
+
# #make_request has been called.
|
46
|
+
attr_reader :response
|
47
|
+
attr_reader :authorization # :nodoc:
|
48
|
+
|
49
|
+
# AWS has some particular rules about how it likes it's form params encoded.
|
50
|
+
def self.aws_encode(value)
|
51
|
+
value.to_s.gsub(/([^a-zA-Z0-9._~-]+)/n) do
|
52
|
+
'%' + $1.unpack('H2' * $1.size).join('%').upcase
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Create a new AwsRequest using the given HTTPClient object.
|
57
|
+
def initialize(httpclient)
|
58
|
+
@httpclient = httpclient
|
59
|
+
end
|
60
|
+
|
61
|
+
# The full URL to the API endpoint the request will call.
|
62
|
+
def url
|
63
|
+
File.join("https://#{service}.#{region}.amazonaws.com", path)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Send the HTTP request and get a response. Returns an AwsResponse object.
|
67
|
+
# The instance is frozen once this method is called and cannot be used
|
68
|
+
# again.
|
69
|
+
def make_request
|
70
|
+
create_authorization
|
71
|
+
|
72
|
+
options = {}
|
73
|
+
options[:header] = authorization.headers.merge(
|
74
|
+
'Authorization' => authorization.authorization_header
|
75
|
+
)
|
76
|
+
options[:query] = query if query.any?
|
77
|
+
options[:body] = form_body if body
|
78
|
+
|
79
|
+
http_response = @httpclient.request(method, url, options)
|
80
|
+
@response = AwsResponse.new(self, http_response)
|
81
|
+
|
82
|
+
freeze
|
83
|
+
|
84
|
+
@response
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# Encode the form params if the body is given as a Hash.
|
90
|
+
def form_body
|
91
|
+
if body.is_a?(Hash)
|
92
|
+
body.map do |k,v|
|
93
|
+
[AwsRequest.aws_encode(k), AwsRequest.aws_encode(v)].join("=")
|
94
|
+
end.join("&")
|
95
|
+
else
|
96
|
+
body
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Create the AwsRequestAuthorization object for the request.
|
101
|
+
def create_authorization
|
102
|
+
@authorization = AwsRequestAuthorization.new.tap do |authorization|
|
103
|
+
authorization.url = url
|
104
|
+
authorization.method = method
|
105
|
+
authorization.query = query
|
106
|
+
authorization.body = form_body
|
107
|
+
authorization.region = region
|
108
|
+
authorization.service = service
|
109
|
+
authorization.credentials = credentials
|
110
|
+
authorization.headers = headers
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require "openssl"
|
2
|
+
|
3
|
+
module Bisques
|
4
|
+
# Instances of this class are used to create HTTP authorization headers for
|
5
|
+
# AWS using Signature Version 4, as per
|
6
|
+
# http://docs.amazonwebservices.com/general/latest/gr/signature-version-4.html
|
7
|
+
#
|
8
|
+
# === Usage
|
9
|
+
#
|
10
|
+
# This is just an example of extracting the relevant information.
|
11
|
+
#
|
12
|
+
# url = "https://sqs.us-east-1.amazon.com/"
|
13
|
+
# original_headers = {"Content-Type": "text/plain"}
|
14
|
+
# query = {"Action" => "ListQueues"}
|
15
|
+
# body = "some body text"
|
16
|
+
# credentials = AwsCredentials.default
|
17
|
+
#
|
18
|
+
# authorization = AwsRequestAuthorization.new
|
19
|
+
# authorization.url = url
|
20
|
+
# authorization.method = "POST"
|
21
|
+
# authorization.query = query
|
22
|
+
# authorization.body = body
|
23
|
+
# authorization.region = "us-east-1"
|
24
|
+
# authorization.service = "sqs"
|
25
|
+
# authorization.headers = original_headers
|
26
|
+
#
|
27
|
+
# new_headers = authorization.headers.merge(
|
28
|
+
# "Authorization" => authorization.authorization_header
|
29
|
+
# )
|
30
|
+
#
|
31
|
+
# url_with_query = url + "?" + query.map{|p| p.join("=")}.join("&")
|
32
|
+
#
|
33
|
+
# Net::HTTP.post(
|
34
|
+
# url_with_query,
|
35
|
+
# body,
|
36
|
+
# new_headers
|
37
|
+
# )
|
38
|
+
#
|
39
|
+
class AwsRequestAuthorization
|
40
|
+
# The full URL to the API call.
|
41
|
+
attr_accessor :url
|
42
|
+
# The HTTP method.
|
43
|
+
attr_accessor :method
|
44
|
+
# A hash of key/pairs for the query string.
|
45
|
+
attr_accessor :query
|
46
|
+
# A string of the body being sent (for POST and PUT HTTP methods).
|
47
|
+
attr_accessor :body
|
48
|
+
# The AWS region.
|
49
|
+
attr_accessor :region
|
50
|
+
# The AWS service.
|
51
|
+
attr_accessor :service
|
52
|
+
# An AwsCredentials object.
|
53
|
+
attr_accessor :credentials
|
54
|
+
# The headers to read back.
|
55
|
+
attr_reader :headers
|
56
|
+
|
57
|
+
# The generated authorization header.
|
58
|
+
def authorization_header
|
59
|
+
[
|
60
|
+
"AWS4-HMAC-SHA256",
|
61
|
+
"Credential=#{credentials.aws_key}/#{request_datestamp}/#{region}/#{service}/aws4_request,",
|
62
|
+
"SignedHeaders=#{signed_headers.join(";")},",
|
63
|
+
"Signature=#{signature.to_s}"
|
64
|
+
].join(" ")
|
65
|
+
end
|
66
|
+
|
67
|
+
# The HTTP headers being sent.
|
68
|
+
def headers=(headers={})
|
69
|
+
@headers = headers.merge(
|
70
|
+
"x-amz-date" => request_timestamp
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# When first called set a time for the request and keep it.
|
77
|
+
def request_time
|
78
|
+
@request_time ||= Time.now.utc
|
79
|
+
end
|
80
|
+
|
81
|
+
# The AWS timestamp format.
|
82
|
+
def request_timestamp
|
83
|
+
request_time.strftime("%Y%m%dT%H%M%SZ")
|
84
|
+
end
|
85
|
+
|
86
|
+
# The AWS datestamp format.
|
87
|
+
def request_datestamp
|
88
|
+
request_time.strftime("%Y%m%d")
|
89
|
+
end
|
90
|
+
|
91
|
+
# The digest used is SHA2.
|
92
|
+
def digest
|
93
|
+
Digest::SHA2.new
|
94
|
+
end
|
95
|
+
|
96
|
+
# Task 3: Calculate the signature.
|
97
|
+
def signature
|
98
|
+
digest = "SHA256"
|
99
|
+
OpenSSL::HMAC.hexdigest(digest, signing_key, string_to_sign)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Calculate the signing key for task 3.
|
103
|
+
def signing_key
|
104
|
+
digest = "SHA256"
|
105
|
+
kDate = OpenSSL::HMAC.digest(digest, "AWS4" + credentials.aws_secret, request_datestamp)
|
106
|
+
kRegion = OpenSSL::HMAC.digest(digest, kDate, region)
|
107
|
+
kService = OpenSSL::HMAC.digest(digest, kRegion, service)
|
108
|
+
OpenSSL::HMAC.digest(digest, kService, "aws4_request")
|
109
|
+
end
|
110
|
+
|
111
|
+
# Task 2: Create the string to sign.
|
112
|
+
def string_to_sign
|
113
|
+
[
|
114
|
+
"AWS4-HMAC-SHA256",
|
115
|
+
request_timestamp,
|
116
|
+
credential_scope,
|
117
|
+
digest.hexdigest(canonical)
|
118
|
+
].join("\n")
|
119
|
+
end
|
120
|
+
|
121
|
+
# Task 1: Create a canonical request.
|
122
|
+
def canonical
|
123
|
+
canonical = ""
|
124
|
+
canonical << method.to_s.upcase << "\n"
|
125
|
+
canonical << uri.path << "\n"
|
126
|
+
|
127
|
+
canonical_query.each_with_index do |(param,value), index|
|
128
|
+
canonical << param << "=" << value
|
129
|
+
canonical << "&" unless index == query.size - 1
|
130
|
+
end
|
131
|
+
|
132
|
+
canonical << "\n"
|
133
|
+
canonical << canonical_headers.map{|h| h.join(":")}.join("\n")
|
134
|
+
canonical << "\n\n"
|
135
|
+
canonical << signed_headers.join(";")
|
136
|
+
canonical << "\n"
|
137
|
+
|
138
|
+
canonical << digest.hexdigest(body.to_s).downcase
|
139
|
+
canonical
|
140
|
+
end
|
141
|
+
|
142
|
+
# List of signed headers.
|
143
|
+
def signed_headers
|
144
|
+
canonical_headers.map{|name,value| name}
|
145
|
+
end
|
146
|
+
|
147
|
+
# Credential scope for task 2.
|
148
|
+
def credential_scope
|
149
|
+
[request_datestamp,
|
150
|
+
region,
|
151
|
+
service,
|
152
|
+
"aws4_request"].join("/")
|
153
|
+
end
|
154
|
+
|
155
|
+
# Canonical query for task 1. Uses the AwsRequest::aws_encode for AWS
|
156
|
+
# encoding rules.
|
157
|
+
def canonical_query
|
158
|
+
query.map{|param,value| [AwsRequest.aws_encode(param), AwsRequest.aws_encode(value)]}.sort
|
159
|
+
end
|
160
|
+
|
161
|
+
# The canonical headers, including the Host.
|
162
|
+
def canonical_headers
|
163
|
+
hash = headers.dup
|
164
|
+
hash["host"] ||= Addressable::URI.parse(url).host
|
165
|
+
hash = hash.map{|name,value| [name.downcase,value]}
|
166
|
+
hash.reject!{|name,value| name == "authorization"}
|
167
|
+
hash.sort
|
168
|
+
end
|
169
|
+
|
170
|
+
def uri
|
171
|
+
Addressable::URI.parse(url)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module Bisques
|
4
|
+
# Created by an AwsRequest to represent the returned details. The original
|
5
|
+
# request is stored in #request. The raw content is available in #content.
|
6
|
+
# AWS returns XML, and a Nokogiri::XML instance is available in #doc.
|
7
|
+
class AwsResponse
|
8
|
+
# The original AwsRequest that created this response.
|
9
|
+
attr_reader :request
|
10
|
+
# The raw response string from AWS
|
11
|
+
attr_reader :content
|
12
|
+
# The HTTP response. This can be used to get any headers or status codes.
|
13
|
+
attr_reader :http_response
|
14
|
+
|
15
|
+
def initialize(request, http_response) # :nodoc:
|
16
|
+
@request = request
|
17
|
+
@http_response = http_response
|
18
|
+
@content = @http_response.body
|
19
|
+
end
|
20
|
+
|
21
|
+
# A Nokogiri::XML document parsed from the #content.
|
22
|
+
def doc
|
23
|
+
@doc ||= Nokogiri::XML(content).tap{|x|x.remove_namespaces!}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns true if the request was successful.
|
27
|
+
def success?
|
28
|
+
@http_response.ok?
|
29
|
+
end
|
30
|
+
|
31
|
+
# The request ID from AWS.
|
32
|
+
def request_id
|
33
|
+
@http_response.header['x-amzn-RequestId']
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'bisques/aws_connection'
|
2
|
+
require 'bisques/aws_credentials'
|
3
|
+
require 'bisques/queue'
|
4
|
+
require 'bisques/queue_listener'
|
5
|
+
require 'digest/md5'
|
6
|
+
|
7
|
+
module Bisques
|
8
|
+
# Bisques is a client for Amazon SQS. All of the API calls made to SQS are
|
9
|
+
# called via methods on this class.
|
10
|
+
#
|
11
|
+
# === Example
|
12
|
+
#
|
13
|
+
# client = Bisques::Client.new('us-east-1', 'my_queues_', AwsCredentials.new(aws_key, aws_secret))
|
14
|
+
# client.list_queues
|
15
|
+
#
|
16
|
+
class Client
|
17
|
+
# The queue prefix when interacting with SQS. The client will only be able
|
18
|
+
# to see queues whose name has this prefix.
|
19
|
+
attr_accessor :queue_prefix
|
20
|
+
|
21
|
+
include AwsConnection
|
22
|
+
|
23
|
+
# Initialize a client object. The AWS region must be specified. For
|
24
|
+
# example, 'us-east-1'. An optional queue prefix can be provided to
|
25
|
+
# restrict the queues this client can see and interact with. AWS
|
26
|
+
# credentials must be provided, or defaults set in AwsCredentials.
|
27
|
+
def initialize(region, queue_prefix = nil, credentials = AwsCredentials.default)
|
28
|
+
super(region, "sqs", credentials)
|
29
|
+
@queue_prefix = queue_prefix
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns a Queue object representing an SQS queue, creating it if it does
|
33
|
+
# not already exist.
|
34
|
+
def get_or_create_queue(name)
|
35
|
+
get_queue(name) || create_queue(name, {})
|
36
|
+
end
|
37
|
+
|
38
|
+
# Creates a new SQS queue and returns a Queue object.
|
39
|
+
def create_queue(name, attributes = {})
|
40
|
+
response = action("CreateQueue", {"QueueName" => Queue.sanitize_name("#{queue_prefix}#{name}")}.merge(attributes))
|
41
|
+
|
42
|
+
if response.success?
|
43
|
+
Queue.new(self, response.doc.xpath("//QueueUrl").text)
|
44
|
+
else
|
45
|
+
raise "Could not create queue #{name}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Deletes an SQS queue at a given path.
|
50
|
+
def delete_queue(queue_url)
|
51
|
+
response = action("DeleteQueue", queue_url)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Get an SQS queue by name. Returns a Queue object if the queue is found, otherwise nil.
|
55
|
+
def get_queue(name, options = {})
|
56
|
+
response = action("GetQueueUrl", {"QueueName" => Queue.sanitize_name("#{queue_prefix}#{name}")}.merge(options))
|
57
|
+
|
58
|
+
if response.success?
|
59
|
+
Queue.new(self, response.doc.xpath("//QueueUrl").text)
|
60
|
+
end
|
61
|
+
|
62
|
+
rescue Bisques::AwsActionError => e
|
63
|
+
raise unless e.code == "AWS.SimpleQueueService.NonExistentQueue"
|
64
|
+
end
|
65
|
+
|
66
|
+
# Return an array of Queue objects representing the queues found in SQS. An
|
67
|
+
# optional prefix can be supplied to restrict the queues found. This prefix
|
68
|
+
# is additional to the client prefix.
|
69
|
+
#
|
70
|
+
# Example:
|
71
|
+
#
|
72
|
+
# # Delete all the queues
|
73
|
+
# client.list_queues.each do |queue|
|
74
|
+
# queue.delete
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
def list_queues(prefix = "")
|
78
|
+
response = action("ListQueues", "QueueNamePrefix" => "#{queue_prefix}#{prefix}")
|
79
|
+
response.doc.xpath("//ListQueuesResult/QueueUrl").map(&:text).map do |url|
|
80
|
+
Queue.new(self, url)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Get the attributes for a queue. Takes an array of attribute names.
|
85
|
+
# Defaults to ["All"] which returns all the available attributes.
|
86
|
+
#
|
87
|
+
# This returns an AwsResponse object.
|
88
|
+
def get_queue_attributes(queue_url, attributes = ["All"])
|
89
|
+
attributes = attributes.map(&:to_s)
|
90
|
+
|
91
|
+
query = Hash[*attributes.each_with_index.map do |attribute, index|
|
92
|
+
["AttributeName.#{index+1}", attribute]
|
93
|
+
end.flatten]
|
94
|
+
|
95
|
+
action("GetQueueAttributes", queue_url, query)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Put a message on a queue. Takes the queue url and the message body, which
|
99
|
+
# should be a string. An optional delay seconds argument can be added if
|
100
|
+
# the message should not become visible immediately.
|
101
|
+
#
|
102
|
+
# Example:
|
103
|
+
#
|
104
|
+
# client.send_message(queue.path, "test message")
|
105
|
+
#
|
106
|
+
def send_message(queue_url, message_body, delay_seconds=nil)
|
107
|
+
options = {"MessageBody" => message_body}
|
108
|
+
options["DelaySeconds"] = delay_seconds if delay_seconds
|
109
|
+
|
110
|
+
tries = 0
|
111
|
+
md5 = Digest::MD5.hexdigest(message_body)
|
112
|
+
|
113
|
+
begin
|
114
|
+
tries += 1
|
115
|
+
response = action("SendMessage", queue_url, options)
|
116
|
+
|
117
|
+
returned_md5 = response.doc.xpath("//MD5OfMessageBody").text
|
118
|
+
raise MessageHasWrongMd5Error.new(message_body, md5, returned_md5) unless md5 == returned_md5
|
119
|
+
rescue MessageHasWrongMd5Error
|
120
|
+
if tries < 2
|
121
|
+
retry
|
122
|
+
else
|
123
|
+
raise
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Delete a message from a queue. The message is deleted by the handle given
|
129
|
+
# when the message is retrieved.
|
130
|
+
def delete_message(queue_url, receipt_handle)
|
131
|
+
response = action("DeleteMessage", queue_url, {"ReceiptHandle" => receipt_handle})
|
132
|
+
end
|
133
|
+
|
134
|
+
# Receive a message from a queue. Takes the queue url and an optional hash.
|
135
|
+
def receive_message(queue_url, options = {})
|
136
|
+
# validate_options(options, %w(AttributeName MaxNumberOfMessages VisibilityTimeout WaitTimeSeconds))
|
137
|
+
action("ReceiveMessage", queue_url, options)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Change the visibility of a message on the queue. This is useful if you
|
141
|
+
# have retrieved a message and now want to keep it hidden for longer before
|
142
|
+
# deleting it, or if you have a job and decide you cannot action it and
|
143
|
+
# want to return it to the queue sooner.
|
144
|
+
def change_message_visibility(queue_url, receipt_handle, visibility_timeout)
|
145
|
+
action("ChangeMessageVisibility", queue_url, {"ReceiptHandle" => receipt_handle, "VisibilityTimeout" => visibility_timeout})
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Bisques
|
4
|
+
# A message received from an SQS queue.
|
5
|
+
class Message
|
6
|
+
# The queue this message originated from.
|
7
|
+
attr_reader :queue
|
8
|
+
# The AWS Id of the message.
|
9
|
+
attr_reader :id
|
10
|
+
# A unique handle used to manipulate the message.
|
11
|
+
attr_reader :handle
|
12
|
+
# The raw text body of the message.
|
13
|
+
attr_reader :body
|
14
|
+
# Hash of SQS attributes.
|
15
|
+
attr_reader :attributes
|
16
|
+
|
17
|
+
def initialize(queue, id, handle, body, attributes = {}) #:nodoc:
|
18
|
+
@queue, @id, @handle, @body, @attributes = queue, id, handle, body, attributes
|
19
|
+
end
|
20
|
+
|
21
|
+
# The deserialized object in the message. This method is used to retrieve
|
22
|
+
# the contents that Queue#post_message placed there.
|
23
|
+
#
|
24
|
+
# Example:
|
25
|
+
#
|
26
|
+
# queue.post_message([1,2,3])
|
27
|
+
# queue.retrieve.object => [1,2,3]
|
28
|
+
#
|
29
|
+
def object
|
30
|
+
@object ||= JSON.parse(body)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Delete the message from the queue. This should be called after the
|
34
|
+
# message has been received and processed. If not then after a timeout the
|
35
|
+
# message will get added back to the queue.
|
36
|
+
def delete
|
37
|
+
queue.delete_message(handle)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return the message to the queue immediately. If a client has taken a
|
41
|
+
# message and cannot process it for any reason it can put the message back
|
42
|
+
# faster than the default timeout by calling this method.
|
43
|
+
def return
|
44
|
+
queue.return_message(handle)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'bisques/message'
|
2
|
+
|
3
|
+
module Bisques
|
4
|
+
# An SQS queue
|
5
|
+
class Queue
|
6
|
+
class QueueError < Bisques::Error
|
7
|
+
def initialize(queue, message)
|
8
|
+
@queue = queue
|
9
|
+
super("queue: #{queue.name}; #{message}")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
class QueueNotFound < QueueError; end
|
13
|
+
|
14
|
+
|
15
|
+
attr_reader :client # :nodoc:
|
16
|
+
|
17
|
+
def self.sanitize_name(name)
|
18
|
+
name = name.gsub(/[^_\w\d]/, "")
|
19
|
+
|
20
|
+
if name.length > 80
|
21
|
+
short_name = name[0,75]
|
22
|
+
short_name << Digest::MD5.hexdigest(name)
|
23
|
+
short_name = short_name[0,80]
|
24
|
+
name = short_name
|
25
|
+
end
|
26
|
+
|
27
|
+
name
|
28
|
+
end
|
29
|
+
|
30
|
+
# Queues are created by the Client passing the client itself and the url
|
31
|
+
# for the queue.
|
32
|
+
def initialize(client, url)
|
33
|
+
@client, @url = client, url
|
34
|
+
end
|
35
|
+
|
36
|
+
# The name of the queue derived from the URL.
|
37
|
+
def name
|
38
|
+
@url.split("/").last
|
39
|
+
end
|
40
|
+
|
41
|
+
# The path part of the queue URL
|
42
|
+
def path
|
43
|
+
Addressable::URI.parse(@url).path
|
44
|
+
end
|
45
|
+
alias_method :url, :path
|
46
|
+
|
47
|
+
def eql?(queue)
|
48
|
+
hash == queue.hash
|
49
|
+
end
|
50
|
+
def ==(queue)
|
51
|
+
hash == queue.hash
|
52
|
+
end
|
53
|
+
def hash # :nodoc:
|
54
|
+
@url.hash
|
55
|
+
end
|
56
|
+
|
57
|
+
# Return attributes for the queue. Pass in the names of the attributes to
|
58
|
+
# retrieve, or :All for all attributes. The available attributes can be
|
59
|
+
# found at
|
60
|
+
# http://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/Query_QueryGetQueueAttributes.html
|
61
|
+
#
|
62
|
+
# If 1 attribute is requested then just that attributes value is returned.
|
63
|
+
# If more than one, or all, attributes are requested then a hash of
|
64
|
+
# attribute names and values is returned.
|
65
|
+
#
|
66
|
+
# ==== Example with one attribute:
|
67
|
+
#
|
68
|
+
# queue.attributes(:ApproximateNumberOfMessages) => 10
|
69
|
+
#
|
70
|
+
# ==== Example with multiple attributes:
|
71
|
+
#
|
72
|
+
# queue.attributes(:ApproximateNumberOfMessages, :ApproximateNumberOfMessagesDelayed) => {:ApproximateNumberOfMessages => 10, :ApproximateNumberOfMessagesDelayed => 5}
|
73
|
+
#
|
74
|
+
def attributes(*attributes)
|
75
|
+
return nil if attributes.blank?
|
76
|
+
|
77
|
+
values = {}
|
78
|
+
response = client.get_queue_attributes(url, attributes)
|
79
|
+
|
80
|
+
response.doc.xpath("//Attribute").each do |attribute_element|
|
81
|
+
name = attribute_element.xpath("Name").text
|
82
|
+
value = attribute_element.xpath("Value").text
|
83
|
+
value = value.to_i if value =~ /\A\d+\z/
|
84
|
+
values[name] = value
|
85
|
+
end
|
86
|
+
|
87
|
+
if values.size == 1 && attributes.size == 1
|
88
|
+
values.values.first
|
89
|
+
else
|
90
|
+
values
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Delete the queue
|
95
|
+
def delete
|
96
|
+
client.delete_queue(url)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Post a message to the queue. The message must be serializable (i.e.
|
100
|
+
# strings, numbers, arrays, hashes).
|
101
|
+
def post_message(object)
|
102
|
+
client.send_message(url, JSON.dump(object))
|
103
|
+
end
|
104
|
+
|
105
|
+
# Retrieve a message from the queue. Returns nil if no message is waiting
|
106
|
+
# in the given poll time. Otherwise it returns a Message.
|
107
|
+
def retrieve(poll_time = 1)
|
108
|
+
response = client.receive_message(url, {"WaitTimeSeconds" => poll_time, "MaxNumberOfMessages" => 1})
|
109
|
+
raise QueueNotFound.new(self, "not found at #{url}") if response.http_response.status == 404
|
110
|
+
|
111
|
+
response.doc.xpath("//Message").map do |element|
|
112
|
+
attributes = Hash[*element.xpath("Attribute").map do |attr_element|
|
113
|
+
[attr_element.xpath("Name").text, attr_element.xpath("Value").text]
|
114
|
+
end.flatten]
|
115
|
+
|
116
|
+
Message.new(self, element.xpath("MessageId").text,
|
117
|
+
element.xpath("ReceiptHandle").text,
|
118
|
+
element.xpath("Body").text,
|
119
|
+
attributes
|
120
|
+
)
|
121
|
+
end.first
|
122
|
+
end
|
123
|
+
|
124
|
+
# Retrieve a single message from the queue. This will block until a message
|
125
|
+
# arrives. The message will be of the class Message.
|
126
|
+
def retrieve_one(poll_time = 5)
|
127
|
+
object = nil
|
128
|
+
while object.nil?
|
129
|
+
object = retrieve(poll_time)
|
130
|
+
end
|
131
|
+
object
|
132
|
+
end
|
133
|
+
|
134
|
+
# Delete a message from the queue. This should be called to confirm that
|
135
|
+
# the message has been processed. If it is not called then the message will
|
136
|
+
# get put back on the queue after a timeout.
|
137
|
+
def delete_message(handle)
|
138
|
+
response = client.delete_message(url, handle)
|
139
|
+
response.success?
|
140
|
+
end
|
141
|
+
|
142
|
+
# Return a message to the queue after receiving it. This would typically
|
143
|
+
# happen if the receiver decided it couldn't process.
|
144
|
+
def return_message(handle)
|
145
|
+
client.change_message_visibility(url, handle, 0)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'bisques/queue'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Bisques
|
5
|
+
# Listen for messages on a queue and execute a block when they arrive.
|
6
|
+
class QueueListener
|
7
|
+
def initialize(queue, poll_time = 5)
|
8
|
+
@queue, @poll_time = queue, poll_time
|
9
|
+
end
|
10
|
+
|
11
|
+
def listening?
|
12
|
+
@listening
|
13
|
+
end
|
14
|
+
|
15
|
+
# Listen for messages. This is asynchronous and returns immediately.
|
16
|
+
#
|
17
|
+
# Ex:
|
18
|
+
#
|
19
|
+
# queue = bisques.find_or_create_queue("my queue")
|
20
|
+
# listener = QueuedListener.new(queue)
|
21
|
+
# listener.listen do |message|
|
22
|
+
# puts "Received #{message.object}"
|
23
|
+
# message.delete
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# while true; sleep 1; end # Process messages forever
|
27
|
+
#
|
28
|
+
# Note that the block you give to this method is executed in a new thread.
|
29
|
+
#
|
30
|
+
def listen(&block)
|
31
|
+
return if @listening
|
32
|
+
@listening = true
|
33
|
+
|
34
|
+
@thread = Thread.new do
|
35
|
+
while @listening
|
36
|
+
message = @queue.retrieve(@poll_time)
|
37
|
+
block.call(message) if message.present?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Stop listening for messages
|
43
|
+
def stop
|
44
|
+
@listening = false
|
45
|
+
@thread.join if @thread
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Listen for messages on several queues at the same time. The interface for
|
50
|
+
# objects of this class is identical to that of QueueListener.
|
51
|
+
#
|
52
|
+
# Ex:
|
53
|
+
#
|
54
|
+
# queue_1 = bisques.find_or_create_queue("queue one")
|
55
|
+
# queue_2 = bisques.find_or_create_queue("queue two")
|
56
|
+
# listener = MultiQueueListener.new(queue_1, queue_2)
|
57
|
+
# listener.listen do |message|
|
58
|
+
# puts "Queue #{message.queue.name}, message #{message.object}"
|
59
|
+
# message.delete
|
60
|
+
# end
|
61
|
+
# while true; sleep 1; end # Process messages forever
|
62
|
+
#
|
63
|
+
class MultiQueueListener
|
64
|
+
def initialize(*queues)
|
65
|
+
@queues = queues
|
66
|
+
@listeners = []
|
67
|
+
end
|
68
|
+
|
69
|
+
def listening?
|
70
|
+
@listeners.any?
|
71
|
+
end
|
72
|
+
|
73
|
+
def listen(&block)
|
74
|
+
return if @listeners.any?
|
75
|
+
@listeners = @queues.map do |queue|
|
76
|
+
QueueListener.new(queue)
|
77
|
+
end
|
78
|
+
|
79
|
+
@listeners.each do |listener|
|
80
|
+
listener.listen(&block)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def stop
|
85
|
+
@listeners.each(&:stop)
|
86
|
+
@listeners = []
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/bisques.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# See README.rdoc
|
2
|
+
module Bisques
|
3
|
+
class Error < StandardError; end
|
4
|
+
class MessageHasWrongMd5Error < Error
|
5
|
+
attr_reader :msg, :expected, :got
|
6
|
+
|
7
|
+
def initialize(msg, expected, got)
|
8
|
+
@msg, @expected, @got = msg, expected, got
|
9
|
+
super(msg)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'bisques/client'
|
metadata
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bisques
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jeremy Wells
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-31 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: httpclient
|
16
|
+
prerelease: false
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
none: false
|
23
|
+
type: :runtime
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ! '>='
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '0'
|
29
|
+
none: false
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: addressable
|
32
|
+
prerelease: false
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
none: false
|
39
|
+
type: :runtime
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
none: false
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: nokogiri
|
48
|
+
prerelease: false
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
none: false
|
55
|
+
type: :runtime
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
none: false
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: json
|
64
|
+
prerelease: false
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
none: false
|
71
|
+
type: :runtime
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
none: false
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rspec
|
80
|
+
prerelease: false
|
81
|
+
requirement: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
none: false
|
87
|
+
type: :development
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
none: false
|
94
|
+
description: AWS SQS client
|
95
|
+
email: jemmyw@gmail.com
|
96
|
+
executables: []
|
97
|
+
extensions: []
|
98
|
+
extra_rdoc_files:
|
99
|
+
- README.rdoc
|
100
|
+
files:
|
101
|
+
- README.rdoc
|
102
|
+
- Rakefile
|
103
|
+
- lib/bisques/aws_connection.rb
|
104
|
+
- lib/bisques/aws_credentials.rb
|
105
|
+
- lib/bisques/aws_request.rb
|
106
|
+
- lib/bisques/aws_request_authorization.rb
|
107
|
+
- lib/bisques/aws_response.rb
|
108
|
+
- lib/bisques/client.rb
|
109
|
+
- lib/bisques/message.rb
|
110
|
+
- lib/bisques/queue.rb
|
111
|
+
- lib/bisques/queue_listener.rb
|
112
|
+
- lib/bisques.rb
|
113
|
+
homepage: https://github.com/bigears/bisques
|
114
|
+
licenses: []
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options:
|
117
|
+
- --line-numbers
|
118
|
+
- --inline-source
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
none: false
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ! '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
none: false
|
133
|
+
requirements: []
|
134
|
+
rubyforge_project:
|
135
|
+
rubygems_version: 1.8.24
|
136
|
+
signing_key:
|
137
|
+
specification_version: 3
|
138
|
+
summary: AWS SQS client
|
139
|
+
test_files: []
|