sucker 1.1.4 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +13 -19
- data/lib/sucker.rb +4 -4
- data/lib/sucker/parameters.rb +28 -0
- data/lib/sucker/request.rb +100 -88
- data/lib/sucker/response.rb +10 -0
- data/lib/sucker/version.rb +1 -1
- data/spec/fixtures/cassette_library/{unit → spec}/sucker/request.yml +4 -4
- data/spec/fixtures/cassette_library/spec/sucker/response.yml +26 -0
- data/spec/sucker/parameters_spec.rb +61 -0
- data/spec/sucker/request_spec.rb +276 -0
- data/spec/{unit/sucker → sucker}/response_spec.rb +39 -60
- data/spec/{unit/sucker_spec.rb → sucker_spec.rb} +0 -4
- data/spec/support/vcr.rb +0 -1
- metadata +69 -114
- data/spec/fixtures/cassette_library/integration/alternate_versions.yml +0 -26
- data/spec/fixtures/cassette_library/integration/errors.yml +0 -26
- data/spec/fixtures/cassette_library/integration/france.yml +0 -26
- data/spec/fixtures/cassette_library/integration/images.yml +0 -26
- data/spec/fixtures/cassette_library/integration/item_lookup/multiple.yml +0 -26
- data/spec/fixtures/cassette_library/integration/item_lookup/single.yml +0 -26
- data/spec/fixtures/cassette_library/integration/item_search.yml +0 -26
- data/spec/fixtures/cassette_library/integration/japan.yml +0 -26
- data/spec/fixtures/cassette_library/integration/keyword_search.yml +0 -26
- data/spec/fixtures/cassette_library/integration/kindle.yml +0 -26
- data/spec/fixtures/cassette_library/integration/kindle_2.yml +0 -26
- data/spec/fixtures/cassette_library/integration/multiple_locales.yml +0 -151
- data/spec/fixtures/cassette_library/integration/power_search.yml +0 -26
- data/spec/fixtures/cassette_library/integration/related_items/child.yml +0 -26
- data/spec/fixtures/cassette_library/integration/related_items/parent.yml +0 -26
- data/spec/fixtures/cassette_library/integration/seller_listings_search.yml +0 -26
- data/spec/fixtures/cassette_library/integration/twenty_items.yml +0 -26
- data/spec/fixtures/cassette_library/unit/sucker/response.yml +0 -26
- data/spec/integration/alternate_versions_spec.rb +0 -35
- data/spec/integration/errors_spec.rb +0 -40
- data/spec/integration/france_spec.rb +0 -42
- data/spec/integration/images_spec.rb +0 -41
- data/spec/integration/item_lookup_spec.rb +0 -71
- data/spec/integration/item_search_spec.rb +0 -41
- data/spec/integration/japan_spec.rb +0 -35
- data/spec/integration/keyword_search_spec.rb +0 -33
- data/spec/integration/kindle_spec.rb +0 -55
- data/spec/integration/multiple_locales_spec.rb +0 -70
- data/spec/integration/power_search_spec.rb +0 -41
- data/spec/integration/related_items_spec.rb +0 -53
- data/spec/integration/seller_listing_search_spec.rb +0 -32
- data/spec/integration/twenty_items_spec.rb +0 -49
- data/spec/unit/sucker/request_spec.rb +0 -282
data/README.md
CHANGED
@@ -1,21 +1,23 @@
|
|
1
1
|
Sucker
|
2
2
|
======
|
3
3
|
|
4
|
-
Sucker is a
|
4
|
+
Sucker is a [cURL-](http://github.com/taf2/curb) and [Nokogiri-](http://github.com/rails/rails/blob/master/activesupport/lib/active_support/xml_mini/nokogiri.rb)driven Ruby wrapper to the [Amazon Product Advertising API](https://affiliate-program.amazon.co.uk/gp/advertising/api/detail/main.html).
|
5
|
+
|
6
|
+
It's fast and supports __the entire API__.
|
5
7
|
|
6
8
|
![Electrolux](https://github.com/papercavalier/sucker/raw/master/electrolux.jpg)
|
7
9
|
|
8
10
|
Usage
|
9
11
|
-----
|
10
12
|
|
11
|
-
|
13
|
+
Where's your worker?
|
12
14
|
|
13
15
|
worker = Sucker.new(
|
14
|
-
:locale =>
|
16
|
+
:locale => :us,
|
15
17
|
:key => "API KEY",
|
16
18
|
:secret => "API SECRET")
|
17
19
|
|
18
|
-
|
20
|
+
Build a query.
|
19
21
|
|
20
22
|
worker << {
|
21
23
|
"Operation" => "ItemLookup",
|
@@ -27,19 +29,15 @@ Get a response.
|
|
27
29
|
|
28
30
|
response = worker.get
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
response.valid?
|
33
|
-
|
34
|
-
Now parse it.
|
32
|
+
Parse.
|
35
33
|
|
36
|
-
|
37
|
-
# parse
|
34
|
+
books = response.map("Item") do |item|
|
35
|
+
# parse
|
38
36
|
end
|
39
37
|
|
40
38
|
Repeat ad infinitum.
|
41
39
|
|
42
|
-
[Check
|
40
|
+
[Check the features](http://relishapp.com/papercavalier/sucker) for more detailed examples.
|
43
41
|
|
44
42
|
Read the source code and dive into the [Amazon API docs](https://affiliate-program.amazon.co.uk/gp/advertising/api/detail/main.html).
|
45
43
|
|
@@ -48,18 +46,14 @@ Stubbing
|
|
48
46
|
|
49
47
|
Use [VCR](http://github.com/myronmarston/vcr) to stub your requests.
|
50
48
|
|
51
|
-
|
49
|
+
[This is how my VCR setup looks like](http://github.com/papercavalier/sucker/blob/master/spec/support/vcr.rb).
|
52
50
|
|
53
51
|
Compatibility
|
54
52
|
-------------
|
55
53
|
|
56
|
-
Specs pass against Ruby 1.8.7 and Ruby 1.9.2.
|
57
|
-
|
58
|
-
Sucker works seamlessly with or without Rails.
|
59
|
-
|
60
|
-
Sucker won't work on JRuby. (We got Curl under the hood. It's worth the trade-off.)
|
54
|
+
Specs pass against Ruby 1.8.7 and Ruby 1.9.2. Sucker has cURL under the hood, so no JRuby.
|
61
55
|
|
62
56
|
Afterword
|
63
57
|
---------
|
64
58
|
|
65
|
-
|
59
|
+
Don't overabstract a spaghetti API.
|
data/lib/sucker.rb
CHANGED
@@ -1,16 +1,16 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'sucker/parameters'
|
2
|
+
require 'sucker/request'
|
3
|
+
require 'sucker/response'
|
3
4
|
|
4
5
|
# = Sucker
|
5
6
|
#
|
6
7
|
# Sucker is a Ruby wrapper to the Amazon Product Advertising API.
|
7
8
|
module Sucker
|
8
|
-
CURRENT_AMAZON_API_VERSION = "2010-10-01"
|
9
9
|
|
10
10
|
# Initializes a request object
|
11
11
|
#
|
12
12
|
# worker = Sucker.new(
|
13
|
-
# :locale =>
|
13
|
+
# :locale => :us,
|
14
14
|
# :key => "API KEY",
|
15
15
|
# :secret => "API SECRET")
|
16
16
|
#
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
3
|
+
module Sucker
|
4
|
+
class Parameters < Hash
|
5
|
+
API_VERSION = '2010-11-01'
|
6
|
+
SERVICE = 'AWSECommerceService'
|
7
|
+
|
8
|
+
def initialize #:nodoc
|
9
|
+
self.store 'Service', SERVICE
|
10
|
+
self.store 'Version', API_VERSION
|
11
|
+
self.store 'Timestamp', timestamp
|
12
|
+
end
|
13
|
+
|
14
|
+
def normalize
|
15
|
+
inject({}) do |h, kv|
|
16
|
+
k, v = kv
|
17
|
+
h[k.to_s.camelize] = v.is_a?(Array) ? v.join(',') : v.to_s
|
18
|
+
h
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def timestamp
|
25
|
+
Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/sucker/request.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
1
|
+
require 'curb'
|
2
|
+
require 'openssl'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'uri'
|
5
5
|
|
6
6
|
module Sucker #:nodoc:
|
7
7
|
|
@@ -16,39 +16,25 @@ module Sucker #:nodoc:
|
|
16
16
|
:jp => 'ecs.amazonaws.jp' }
|
17
17
|
PATH = "/onca/xml"
|
18
18
|
|
19
|
-
# The Amazon locale to query
|
20
|
-
attr_accessor :locale
|
21
|
-
|
22
19
|
# The Amazon secret access key
|
23
20
|
attr_accessor :secret
|
24
21
|
|
25
|
-
# The hash of parameters to query Amazon with
|
26
|
-
attr_accessor :parameters
|
27
|
-
|
28
22
|
# Initializes a request object
|
29
23
|
#
|
30
24
|
# worker = Sucker.new(
|
31
|
-
# :locale => "us",
|
32
25
|
# :key => "API KEY",
|
33
26
|
# :secret => "API SECRET")
|
34
27
|
#
|
35
28
|
def initialize(args)
|
36
|
-
self.parameters = {
|
37
|
-
"Service" => "AWSECommerceService",
|
38
|
-
"Version" => CURRENT_AMAZON_API_VERSION
|
39
|
-
}
|
40
|
-
|
41
29
|
args.each { |k, v| send("#{k}=", v) }
|
42
30
|
end
|
43
31
|
|
44
|
-
# Merges a hash into existing parameters
|
32
|
+
# Merges a hash into the existing parameters
|
45
33
|
#
|
46
|
-
# worker = Sucker.new
|
47
34
|
# worker << {
|
48
|
-
# "Operation"
|
49
|
-
# "IdType"
|
50
|
-
# "ItemId"
|
51
|
-
# "ResponseGroup" => "ItemAttributes" }
|
35
|
+
# "Operation" => "ItemLookup",
|
36
|
+
# "IdType" => "ASIN",
|
37
|
+
# "ItemId" => "0816614024" }
|
52
38
|
#
|
53
39
|
def <<(hash)
|
54
40
|
self.parameters.merge!(hash)
|
@@ -56,12 +42,11 @@ module Sucker #:nodoc:
|
|
56
42
|
|
57
43
|
# Returns the associate tag for the current locale
|
58
44
|
def associate_tag
|
59
|
-
associate_tags[locale.to_sym] rescue
|
45
|
+
@associate_tags[locale.to_sym] rescue ''
|
60
46
|
end
|
61
47
|
|
62
48
|
# Sets the associate tag for the current locale
|
63
49
|
#
|
64
|
-
# worker = Sucker.new
|
65
50
|
# worker.associate_tag = 'foo-bar'
|
66
51
|
#
|
67
52
|
def associate_tag=(token)
|
@@ -80,10 +65,8 @@ module Sucker #:nodoc:
|
|
80
65
|
# tags = {
|
81
66
|
# :us => 'foo-bar-10',
|
82
67
|
# :uk => 'foo-bar-20',
|
83
|
-
# :de => 'foo-bar-30',
|
84
68
|
# ... }
|
85
69
|
#
|
86
|
-
# worker = Sucker.new
|
87
70
|
# worker.associate_tags = tags
|
88
71
|
#
|
89
72
|
def associate_tags=(tokens)
|
@@ -92,7 +75,6 @@ module Sucker #:nodoc:
|
|
92
75
|
|
93
76
|
# Returns options for curl and yields them if given a block
|
94
77
|
#
|
95
|
-
# worker = Sucker.new
|
96
78
|
# worker.curl_opts { |c| c.interface = "eth1" }
|
97
79
|
#
|
98
80
|
def curl_opts
|
@@ -104,61 +86,56 @@ module Sucker #:nodoc:
|
|
104
86
|
|
105
87
|
# Performs a request and returns a response
|
106
88
|
#
|
107
|
-
# worker = Sucker.new
|
108
89
|
# response = worker.get
|
109
90
|
#
|
110
|
-
|
111
|
-
|
112
|
-
raise ArgumentError.new "AWS access key missing" unless key
|
113
|
-
|
114
|
-
curl = Curl::Easy.perform(uri.to_s) do |easy|
|
115
|
-
curl_opts.each { |k, v| easy.send("#{k}=", v) }
|
116
|
-
end
|
117
|
-
|
118
|
-
Response.new(curl)
|
119
|
-
end
|
120
|
-
|
121
|
-
# Performs a request for all locales, returns an array of responses, and yields
|
122
|
-
# them if given a block
|
123
|
-
#
|
124
|
-
# worker = Sucker.new
|
91
|
+
# Optionally, pass one or more locales to query specific locales or `:all`
|
92
|
+
# to query all locales.
|
125
93
|
#
|
126
|
-
#
|
127
|
-
# responses = worker.get_all
|
94
|
+
# responses = worker.get(:us)
|
128
95
|
#
|
129
|
-
#
|
130
|
-
# worker.get_all do |response|
|
131
|
-
# process_response
|
132
|
-
# end
|
96
|
+
# responses = worker.get(:all)
|
133
97
|
#
|
134
|
-
def
|
135
|
-
|
136
|
-
self.locale = locale
|
137
|
-
uri.to_s
|
138
|
-
end
|
139
|
-
responses = []
|
98
|
+
def get(*args)
|
99
|
+
case args.count
|
140
100
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
101
|
+
when 0
|
102
|
+
curl = Curl::Easy.perform(uri.to_s) do |easy|
|
103
|
+
curl_opts.each { |k, v| easy.send("#{k}=", v) }
|
104
|
+
end
|
105
|
+
Response.new(curl)
|
106
|
+
|
107
|
+
when 1
|
108
|
+
arg = args.first
|
109
|
+
if arg == :all
|
110
|
+
get_multi locales
|
111
|
+
else
|
112
|
+
self.locale = arg
|
113
|
+
get
|
114
|
+
end
|
115
|
+
|
116
|
+
else
|
117
|
+
get_multi args
|
145
118
|
end
|
119
|
+
end
|
146
120
|
|
147
|
-
|
121
|
+
def get_all # :nodoc:
|
122
|
+
warn "[DEPRECATION] `get_all` is deprecated. Please use `get(:all) instead."
|
123
|
+
get(:all)
|
148
124
|
end
|
149
125
|
|
150
126
|
# Returns the AWS access key for the current locale
|
151
127
|
def key
|
152
|
-
@keys[locale
|
128
|
+
raise ArgumentError.new "AWS access key missing" unless @keys[locale]
|
129
|
+
|
130
|
+
@keys[locale]
|
153
131
|
end
|
154
132
|
|
155
|
-
# Sets a global AWS access key
|
133
|
+
# Sets a global AWS access key
|
156
134
|
#
|
157
|
-
# worker = Sucker.new
|
158
135
|
# worker.key = 'foo'
|
159
136
|
#
|
160
137
|
def key=(token)
|
161
|
-
@keys =
|
138
|
+
@keys = locales.inject({}) do |keys, locale|
|
162
139
|
keys[locale] = token
|
163
140
|
keys
|
164
141
|
end
|
@@ -170,63 +147,95 @@ module Sucker #:nodoc:
|
|
170
147
|
# and Canada and (2) the UK, France, and Germany count against the same call
|
171
148
|
# rate quota.
|
172
149
|
#
|
173
|
-
#
|
150
|
+
# keys = {
|
174
151
|
# :us => 'foo',
|
175
152
|
# :uk => 'bar',
|
176
|
-
# :de => 'baz',
|
177
153
|
# ... }
|
178
154
|
#
|
179
|
-
# worker = Sucker.new
|
180
155
|
# worker.keys = keys
|
181
156
|
#
|
182
157
|
def keys=(tokens)
|
183
158
|
@keys = tokens
|
184
159
|
end
|
185
160
|
|
161
|
+
def locale
|
162
|
+
raise ArgumentError.new "Locale not set" unless @locale
|
163
|
+
|
164
|
+
@locale
|
165
|
+
end
|
166
|
+
|
167
|
+
# Sets the current Amazon locale
|
168
|
+
#
|
169
|
+
# Valid values are :us, :uk, :de, :ca, :fr, and :jp.
|
170
|
+
def locale=(new_locale)
|
171
|
+
new_locale = new_locale.to_sym
|
172
|
+
|
173
|
+
raise ArgumentError.new "Invalid locale" unless locales.include? new_locale
|
174
|
+
|
175
|
+
@locale = new_locale
|
176
|
+
end
|
177
|
+
|
178
|
+
# The parameters to query Amazon with
|
179
|
+
def parameters
|
180
|
+
@parameters ||= Parameters.new
|
181
|
+
end
|
182
|
+
|
186
183
|
# Sets the Amazon API version
|
187
184
|
#
|
188
|
-
# worker = Sucker.new
|
189
185
|
# worker.version = '2010-06-01'
|
190
186
|
#
|
191
187
|
def version=(version)
|
192
|
-
self.parameters[
|
188
|
+
self.parameters['Version'] = version
|
193
189
|
end
|
194
190
|
|
195
191
|
private
|
196
192
|
|
197
|
-
# Timestamps parameters and concatenates them into a query string
|
198
193
|
def build_query
|
199
194
|
parameters.
|
200
|
-
|
201
|
-
merge({
|
202
|
-
merge({
|
195
|
+
normalize.
|
196
|
+
merge({ 'AWSAccessKeyId' => key }).
|
197
|
+
merge({ 'AssociateTag' => associate_tag }).
|
203
198
|
sort.
|
204
|
-
|
205
|
-
"#{k}=" + escape(v
|
206
|
-
end.
|
207
|
-
join("&")
|
199
|
+
map do |k, v|
|
200
|
+
"#{k}=" + escape(v)
|
201
|
+
end.join('&')
|
208
202
|
end
|
209
203
|
|
210
|
-
# Returns a signed and timestamped query string
|
211
204
|
def build_signed_query
|
212
205
|
query = build_query
|
213
206
|
|
214
|
-
digest = OpenSSL::Digest::Digest.new(
|
215
|
-
string = [
|
207
|
+
digest = OpenSSL::Digest::Digest.new('sha256')
|
208
|
+
string = ['GET', host, PATH, query].join("\n")
|
216
209
|
hmac = OpenSSL::HMAC.digest(digest, secret, string)
|
210
|
+
signature = escape([hmac].pack('m').chomp)
|
211
|
+
|
212
|
+
query + '&Signature=' + signature
|
213
|
+
end
|
217
214
|
|
218
|
-
|
215
|
+
def escape(value)
|
216
|
+
value.gsub(/([^a-zA-Z0-9_.~-]+)/) do
|
217
|
+
'%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
|
218
|
+
end
|
219
219
|
end
|
220
220
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
221
|
+
def get_multi(locales)
|
222
|
+
responses = []
|
223
|
+
|
224
|
+
Curl::Multi.get(uris, curl_opts) do |curl|
|
225
|
+
response = Response.new(curl)
|
226
|
+
yield response if block_given?
|
227
|
+
responses << response
|
225
228
|
end
|
229
|
+
|
230
|
+
responses
|
226
231
|
end
|
227
232
|
|
228
233
|
def host
|
229
|
-
HOSTS[locale
|
234
|
+
HOSTS[locale]
|
235
|
+
end
|
236
|
+
|
237
|
+
def locales
|
238
|
+
@locales ||= HOSTS.keys
|
230
239
|
end
|
231
240
|
|
232
241
|
def uri
|
@@ -236,8 +245,11 @@ module Sucker #:nodoc:
|
|
236
245
|
:query => build_signed_query)
|
237
246
|
end
|
238
247
|
|
239
|
-
def
|
240
|
-
|
248
|
+
def uris
|
249
|
+
locales.map do |locale|
|
250
|
+
self.locale = locale
|
251
|
+
uri.to_s
|
252
|
+
end
|
241
253
|
end
|
242
254
|
end
|
243
255
|
end
|
data/lib/sucker/response.rb
CHANGED
@@ -32,6 +32,11 @@ module Sucker #:nodoc:
|
|
32
32
|
find(path).each { |e| yield e }
|
33
33
|
end
|
34
34
|
|
35
|
+
# Returns an array of errors in the reponse
|
36
|
+
def errors
|
37
|
+
find("Error")
|
38
|
+
end
|
39
|
+
|
35
40
|
# Queries an xpath and returns an array of matching nodes
|
36
41
|
#
|
37
42
|
# response = worker.get
|
@@ -41,6 +46,11 @@ module Sucker #:nodoc:
|
|
41
46
|
xml.xpath("//xmlns:#{path}").map { |e| strip_content(e.to_hash[path]) }
|
42
47
|
end
|
43
48
|
|
49
|
+
# Returns true if response contains errors
|
50
|
+
def has_errors?
|
51
|
+
errors.count > 0
|
52
|
+
end
|
53
|
+
|
44
54
|
# A shorthand that yields matches to a block and collects returned values
|
45
55
|
#
|
46
56
|
# descriptions = worker.get.map("Item") { |item| build_description(item) }
|