sucker 1.1.4 → 1.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/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
|

|
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) }
|