amazonian 0.1.0 → 0.2.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/.gitignore +1 -0
- data/README.rdoc +16 -8
- data/lib/amazonian.rb +123 -38
- data/lib/amazonian/version.rb +1 -1
- metadata +4 -4
data/.gitignore
CHANGED
data/README.rdoc
CHANGED
@@ -1,10 +1,7 @@
|
|
1
1
|
== Status
|
2
|
-
This project is in active development as of 2010-
|
3
|
-
|
4
|
-
Currently it can be used as a rails plugin, and maybe as a gem (untested).
|
2
|
+
This project is in active development as of 2010-12.
|
5
3
|
|
6
4
|
== Installation
|
7
|
-
|
8
5
|
Copy or symlink into your application's /lib folder.
|
9
6
|
|
10
7
|
In your ApplicationController:
|
@@ -16,20 +13,31 @@ In your ApplicationController:
|
|
16
13
|
Then create an initializer in config/initializers:
|
17
14
|
require 'amazonian'
|
18
15
|
Amazonian.setup do |config|
|
19
|
-
|
20
|
-
config.
|
21
|
-
|
16
|
+
#Put your Amazon Web Services Key and Secret Key in here
|
17
|
+
config.secret = '<>'
|
18
|
+
config.key = '<>'
|
19
|
+
|
20
|
+
# prints more debug info to the console. Default: false
|
21
|
+
# config.debug = true
|
22
|
+
|
23
|
+
# the default default_search is :All, other tempting options: :Music, :GourmetFood, etc
|
24
|
+
config.default_search = :Music
|
25
|
+
|
26
|
+
# the gem will attempt to filter out duplicate requests. Default: true
|
27
|
+
config.cache_last = true
|
22
28
|
end
|
23
29
|
|
30
|
+
|
24
31
|
== Usage
|
25
32
|
Amazonian.asin "1430218150"
|
26
33
|
#=> #<Amazonian::Item>
|
27
34
|
|
28
|
-
#soon:
|
29
35
|
Amazonian.search "The art of Computer Programming" #Title
|
30
36
|
Amazonian.search "978-0201853926" #ISBN-13
|
31
37
|
Amazonian.search "0201853922" #ISBN-10
|
32
38
|
Amazonian.search "<whateveryouwant>" #Anything you might put in the search box on Amazon.com.
|
39
|
+
#=> #<Amazonian::Search>
|
40
|
+
|
33
41
|
|
34
42
|
== Info
|
35
43
|
|
data/lib/amazonian.rb
CHANGED
@@ -2,48 +2,84 @@ require 'patron'
|
|
2
2
|
require 'crack'
|
3
3
|
require 'hashie'
|
4
4
|
|
5
|
+
#
|
6
|
+
# This module is designed to allow easier querying of the Amazon Product Advertising API
|
7
|
+
# from within your Ruby or Rails applications.
|
8
|
+
#
|
9
|
+
# Basic usage requires first calling +Amazonian.setup+ to provide your Amazon AWS Key and
|
10
|
+
# Secret key for the module to use in querying the database.
|
11
|
+
#
|
12
|
+
# Amazons internal product ids can be used to retrieve data from the API with +Amazonian.asin+.
|
13
|
+
#
|
14
|
+
# Searching for products is done via +Amazonian.search+
|
15
|
+
#
|
16
|
+
# @author Robert L. Carpenter (Modern codebase, interface, gem-ability)
|
17
|
+
# @author phoet (Original ASIN interface and implementation of Request Signing, etc.) https://github.com/phoet/asin
|
18
|
+
#
|
19
|
+
# @api Amazon Web Services Product Advertising API
|
20
|
+
#
|
21
|
+
# @version 0.2.0
|
22
|
+
#
|
5
23
|
module Amazonian
|
24
|
+
|
6
25
|
#worker objects
|
7
26
|
@@digest = OpenSSL::Digest::Digest.new('sha256')
|
8
27
|
@@logger = Logger.new(STDERR)
|
9
28
|
@@patron = Patron::Session.new
|
10
29
|
|
11
30
|
#hold the most recent request/response
|
12
|
-
|
13
|
-
|
14
|
-
|
31
|
+
@request = nil
|
32
|
+
@response = nil
|
33
|
+
@query = nil
|
15
34
|
|
16
35
|
#configuration variables
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
36
|
+
@host = 'webservices.amazon.com'
|
37
|
+
@path = '/onca/xml'
|
38
|
+
@debug = true
|
39
|
+
@key = ''
|
40
|
+
@secret = ''
|
41
|
+
@default_search = :All
|
42
|
+
@cache_last = true
|
22
43
|
|
23
44
|
mattr_reader :host,:path,:response,:request
|
24
|
-
mattr_accessor :key,:secret,:debug
|
45
|
+
mattr_accessor :key,:secret,:debug,:default_search,:cache_last
|
25
46
|
|
26
47
|
#Configure Patron
|
27
48
|
@@patron.timeout = 10
|
28
49
|
|
50
|
+
#
|
29
51
|
# Configure the basic request parameters for Amazonian.
|
52
|
+
#
|
53
|
+
# Pass in a block with 1 parameter and modify configuration
|
54
|
+
# variables from there.
|
55
|
+
#
|
56
|
+
# @yield [amazonian] Configuration code block.
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# require 'Amazonian'
|
60
|
+
# Amazonian.setup do |ama|
|
61
|
+
# ama.key = "my awesome key for AWS"
|
62
|
+
# ama.secret = "super secret secret key for AWS"
|
63
|
+
# ama.debug = true
|
64
|
+
# ama.default_search = :Music
|
65
|
+
# ama.cache_last = false
|
66
|
+
# end
|
67
|
+
#
|
30
68
|
def self.setup
|
31
69
|
yield self if block_given?
|
32
70
|
end
|
33
71
|
|
34
|
-
# Performs an +ItemLookup+ REST call against the Amazon API.
|
35
|
-
#
|
36
|
-
# Expects an ASIN (Amazon Standard Identification Number) and returns an +Item+:
|
37
72
|
#
|
38
|
-
#
|
39
|
-
# item.title
|
40
|
-
# => "Learn Objective-C on the Mac (Learn Series)"
|
73
|
+
# Perform an ASIN (Amazon Standard Identification Number) lookup.
|
41
74
|
#
|
42
|
-
#
|
75
|
+
# @param [String] The ASIN with which to query the API.
|
76
|
+
# @param [Hash] Additional options to be passed to the API.
|
77
|
+
# @option params [Symbol] :Operation defaults to :ItemLookup
|
78
|
+
# @option params [Symbol] :ItemLookup defaults to the ASIN passed as param 1.
|
43
79
|
#
|
44
|
-
#
|
80
|
+
# @see For more information on the parameters the API accepts, see http://docs.amazonwebservices.com/AWSEcommerceService/4-0/
|
45
81
|
#
|
46
|
-
#
|
82
|
+
# @return [Amazonian::Item] Representing the response from the API
|
47
83
|
#
|
48
84
|
def self.asin(asin, params={})
|
49
85
|
params = params.merge :Operation => :ItemLookup, :ItemId => asin
|
@@ -51,61 +87,87 @@ module Amazonian
|
|
51
87
|
Item.new xml['ItemLookupResponse']['Items']['Item']
|
52
88
|
end
|
53
89
|
|
90
|
+
#
|
91
|
+
# Perform a search query to the API. This is basically the same thing as
|
92
|
+
# searching with the Amazon website.
|
93
|
+
#
|
94
|
+
# @param [String] The search query
|
95
|
+
# @param [Hash] Additional options to be passed to the API
|
96
|
+
# @option params [Symbol] :Operation defaults to :ItemSearch
|
97
|
+
# @option params [Symbol] :Keywords defaults to the passed search query
|
98
|
+
#
|
99
|
+
# @see For more information on the parameters the API accepts, see http://docs.amazonwebservices.com/AWSEcommerceService/4-0/
|
100
|
+
#
|
101
|
+
# @return [Amazonian::Search] Representing the response from the API. Items returned by the search query are represented as
|
102
|
+
# +Amazonian::Item+ inside +Amazonian::Search+
|
103
|
+
#
|
54
104
|
def self.search(query, params={})
|
55
105
|
params = params.merge :Operation => :ItemSearch,
|
56
106
|
:Keywords => query
|
57
107
|
|
58
|
-
params[:SearchIndex] =
|
108
|
+
params[:SearchIndex] = @default_search if params[:SearchIndex].nil?
|
59
109
|
|
60
110
|
xml = self.call params
|
61
111
|
Search.new xml['ItemSearchResponse']
|
62
112
|
end
|
63
113
|
|
64
|
-
|
114
|
+
private
|
65
115
|
|
116
|
+
#
|
117
|
+
# Director function. Builds out the Request, Signs it, Sends it off, and Parses its XML all via small helper functions.
|
118
|
+
#
|
119
|
+
# @param params [Hash] All of the parameters to be formatted into the API REST call.
|
120
|
+
# @return [Crack::XML] The Parsed XML
|
121
|
+
#
|
66
122
|
def self.call(params)
|
67
|
-
raise "Cannot call the Amazon API without key and secret key." if
|
123
|
+
raise "Cannot call the Amazon API without key and secret key." if @key.blank? || @secret.blank?
|
68
124
|
|
69
125
|
#get the signed query, and assemble the querystring
|
70
|
-
log :debug, "Started Amazonian request for params: #{params.map {|p| "#{p[0]}=>#{p[1]}" }.join ','}" if
|
126
|
+
log :debug, "Started Amazonian request for params: #{params.map {|p| "#{p[0]}=>#{p[1]}" }.join ','}" if @debug
|
71
127
|
|
72
128
|
#memoize the last request for faster API querying...
|
73
129
|
query = assemble_querystring params
|
74
|
-
if query ==
|
130
|
+
if @cache_last && query == @query && @response.body
|
75
131
|
log :debug, "MEMO'D! Shortcutting API call for dup request."
|
76
|
-
return Crack::XML.parse
|
132
|
+
return Crack::XML.parse @response.body
|
77
133
|
end
|
78
|
-
|
134
|
+
@query = query
|
79
135
|
|
80
136
|
#sign the query
|
81
137
|
signed = sign_query query
|
82
138
|
|
83
139
|
#assemble the full URL
|
84
|
-
|
140
|
+
@request = "http://#{@host}#{@path}?#{signed}"
|
85
141
|
|
86
142
|
#make the call
|
87
|
-
log :info, "performing rest call to '#{
|
88
|
-
|
143
|
+
log :info, "performing rest call to '#{@request}'" if @debug
|
144
|
+
@response = @@patron.get @request
|
89
145
|
|
90
|
-
log :debug, "Response Code: #{
|
146
|
+
log :debug, "Response Code: #{@response.status}" if @debug
|
91
147
|
|
92
148
|
#todo, this memo logic is broken....an error code is not always without a body
|
93
|
-
log :error, "Amazon API Error: #{
|
149
|
+
log :error, "Amazon API Error: #{@response.status}" if @response.status >= 400
|
94
150
|
|
95
151
|
#parse the response and return it
|
96
|
-
Crack::XML.parse
|
152
|
+
Crack::XML.parse @response.body
|
97
153
|
end
|
98
154
|
|
155
|
+
#
|
156
|
+
# Builds out a Query String from a hash of symbols.
|
157
|
+
#
|
99
158
|
def self.assemble_querystring(params)
|
100
159
|
# Nice tutorial http://cloudcarpenters.com/blog/amazon_products_api_request_signing/
|
101
160
|
params[:Service] = :AWSECommerceService
|
102
|
-
params[:AWSAccessKeyId] =
|
161
|
+
params[:AWSAccessKeyId] = @key
|
103
162
|
|
104
163
|
# CGI escape each param
|
105
164
|
# signing needs to order the query alphabetically
|
106
165
|
params.map{|key, value| "#{key}=#{CGI.escape(value.to_s)}" }.sort.join('&').gsub('+','%20')
|
107
166
|
end
|
108
167
|
|
168
|
+
#
|
169
|
+
# Signs a query string
|
170
|
+
#
|
109
171
|
def self.sign_query(query)
|
110
172
|
#make a copy... fixme fixme I'm awkward
|
111
173
|
q = query.clone
|
@@ -114,27 +176,30 @@ module Amazonian
|
|
114
176
|
|
115
177
|
# Sign the entire get-request (not just the querystring)
|
116
178
|
# possible gotcha if Patron starts using more/different headers.
|
117
|
-
request_to_sign = "GET\n#{
|
179
|
+
request_to_sign = "GET\n#{@host}\n#{@path}\n#{q}"
|
118
180
|
|
119
181
|
"#{q}&Signature=#{sign_request request_to_sign}"
|
120
182
|
end
|
121
183
|
|
184
|
+
#
|
185
|
+
# Signs an entire request string
|
186
|
+
#
|
122
187
|
def self.sign_request(request_to_sign)
|
123
188
|
# Sign it.
|
124
|
-
hmac = OpenSSL::HMAC.digest(
|
189
|
+
hmac = OpenSSL::HMAC.digest(@digest, @secret, request_to_sign)
|
125
190
|
|
126
191
|
# Don't forget to remove the newline from base64
|
127
192
|
CGI.escape(Base64.encode64(hmac).chomp)
|
128
193
|
end
|
129
194
|
|
130
195
|
def self.log(severity, message)
|
131
|
-
|
196
|
+
@logger.send severity, message if @logger
|
132
197
|
end
|
133
198
|
|
134
199
|
|
135
200
|
# =Item
|
136
201
|
#
|
137
|
-
# The +Item+ class is
|
202
|
+
# The +Item+ class is used to neatly box up the Amazon REST API responses into an Object.
|
138
203
|
#
|
139
204
|
# A Hashie::Mash is used for the internal data representation and can be accessed over the +raw+ attribute.
|
140
205
|
#
|
@@ -145,16 +210,36 @@ module Amazonian
|
|
145
210
|
end
|
146
211
|
|
147
212
|
def title
|
148
|
-
@raw.ItemAttributes.Title
|
213
|
+
if @raw.ItemAttributes && @raw.ItemAttributes.Title
|
214
|
+
@raw.ItemAttributes.Title
|
215
|
+
else
|
216
|
+
nil
|
217
|
+
end
|
149
218
|
end
|
150
219
|
end
|
151
220
|
|
221
|
+
# =Search
|
222
|
+
# The +Search+ class is used to neatly box up the Amazon REST API responses into Objects.
|
223
|
+
#
|
224
|
+
# When using Amazonian.search you should receive an Search object. Search also attempts
|
225
|
+
# to autoboxe all of the Items returned in the search results into an array of +Amazonian::Item+
|
226
|
+
# objects.
|
227
|
+
#
|
228
|
+
# A Hashie::Mash is used for the internal data representation and can be accessed over the +raw+ attribute.
|
229
|
+
#
|
152
230
|
class Search
|
153
231
|
attr_reader :items
|
154
232
|
def initialize(hash)
|
155
233
|
@raw = Hashie::Mash.new(hash)
|
156
234
|
@items = []
|
157
|
-
|
235
|
+
|
236
|
+
if @raw.Items && @raw.Items.Item
|
237
|
+
if @raw.Items.TotalResults.to_i > 1
|
238
|
+
@raw.Items.Item.each {|i| @items.push Amazonian::Item.new(i) }
|
239
|
+
else
|
240
|
+
@items.push Amazonian::Item.new(@raw.Items.Item)
|
241
|
+
end
|
242
|
+
end
|
158
243
|
end
|
159
244
|
end
|
160
245
|
|
data/lib/amazonian/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: amazonian
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 2
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 0.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Robert L. Carpenter
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-
|
18
|
+
date: 2010-12-05 00:00:00 -07:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|