bisques 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/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: []
|