vacuum 2.2.0 → 3.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +96 -128
- data/lib/vacuum.rb +8 -2
- data/lib/vacuum/locale.rb +64 -0
- data/lib/vacuum/matcher.rb +71 -0
- data/lib/vacuum/operation.rb +75 -0
- data/lib/vacuum/request.rb +165 -98
- data/lib/vacuum/resource.rb +62 -0
- data/lib/vacuum/response.rb +21 -9
- data/lib/vacuum/version.rb +1 -1
- data/test/cassettes/vacuum.yml +1904 -765
- data/test/helper.rb +10 -0
- data/test/integration/test_requests.rb +62 -0
- data/test/integration_helper.rb +51 -0
- data/test/locales.rb +23 -0
- data/test/locales.yml +28 -0
- data/test/locales.yml.example +28 -0
- data/test/unit/test_locale.rb +43 -0
- data/test/unit/test_operation.rb +31 -0
- data/test/unit/test_request.rb +61 -0
- data/test/unit/test_resource.rb +12 -0
- data/test/unit/test_response.rb +26 -0
- metadata +58 -20
- data/test/test_integration.rb +0 -69
- data/test/test_unit.rb +0 -106
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aws-sigv4'
|
4
|
+
require 'http'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Vacuum
|
8
|
+
# An Amazon Product Advertising API operation
|
9
|
+
class Operation
|
10
|
+
# @!visibility private
|
11
|
+
attr_reader :locale, :name, :params
|
12
|
+
|
13
|
+
# Creates a new operation
|
14
|
+
#
|
15
|
+
# @param [String] name
|
16
|
+
# @param [Hash] params
|
17
|
+
# @param [Locale] locale
|
18
|
+
def initialize(name, params:, locale:)
|
19
|
+
@name = name
|
20
|
+
@params = params
|
21
|
+
@locale = locale
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [Hash]
|
25
|
+
def headers
|
26
|
+
signature.headers.merge(
|
27
|
+
'x-amz-target' =>
|
28
|
+
"com.amazon.paapi5.v1.ProductAdvertisingAPIv1.#{name}",
|
29
|
+
'content-encoding' => 'amz-1.0',
|
30
|
+
'content-type' => 'application/json; charset=utf-8'
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [String]
|
35
|
+
def body
|
36
|
+
@body ||= build_body
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [String]
|
40
|
+
def url
|
41
|
+
@url ||= build_url
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def build_body
|
47
|
+
hsh = { 'PartnerTag' => locale.partner_tag,
|
48
|
+
'PartnerType' => locale.partner_type }
|
49
|
+
|
50
|
+
params.each do |key, val|
|
51
|
+
key = key.to_s.split('_')
|
52
|
+
.map { |word| word == 'asin' ? 'ASIN' : word.capitalize }.join
|
53
|
+
hsh[key] = val
|
54
|
+
end
|
55
|
+
|
56
|
+
JSON.generate(hsh)
|
57
|
+
end
|
58
|
+
|
59
|
+
def build_url
|
60
|
+
"https://#{locale.host}/paapi5/#{name.downcase}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def signature
|
64
|
+
signer.sign_request(http_method: 'POST', url: url, body: body)
|
65
|
+
end
|
66
|
+
|
67
|
+
def signer
|
68
|
+
Aws::Sigv4::Signer.new(service: 'ProductAdvertisingAPI',
|
69
|
+
region: locale.region,
|
70
|
+
access_key_id: locale.access_key,
|
71
|
+
secret_access_key: locale.secret_key,
|
72
|
+
http_method: 'POST', endpoint: locale.host)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/vacuum/request.rb
CHANGED
@@ -1,122 +1,189 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'http'
|
4
|
+
|
5
|
+
require 'vacuum/locale'
|
6
|
+
require 'vacuum/operation'
|
7
|
+
require 'vacuum/resource'
|
4
8
|
require 'vacuum/response'
|
5
9
|
|
6
10
|
module Vacuum
|
7
|
-
#
|
11
|
+
# A request to the Amazon Product Advertising API
|
8
12
|
class Request
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
'CN' => 'webservices.amazon.cn',
|
20
|
-
'DE' => 'webservices.amazon.de',
|
21
|
-
'ES' => 'webservices.amazon.es',
|
22
|
-
'FR' => 'webservices.amazon.fr',
|
23
|
-
'GB' => 'webservices.amazon.co.uk',
|
24
|
-
'IN' => 'webservices.amazon.in',
|
25
|
-
'IT' => 'webservices.amazon.it',
|
26
|
-
'JP' => 'webservices.amazon.co.jp',
|
27
|
-
'MX' => 'webservices.amazon.com.mx',
|
28
|
-
'TR' => 'webservices.amazon.com.tr',
|
29
|
-
'US' => 'webservices.amazon.com'
|
30
|
-
}.freeze
|
31
|
-
|
32
|
-
OPERATIONS = %w[
|
33
|
-
BrowseNodeLookup
|
34
|
-
CartAdd
|
35
|
-
CartClear
|
36
|
-
CartCreate
|
37
|
-
CartGet
|
38
|
-
CartModify
|
39
|
-
ItemLookup
|
40
|
-
ItemSearch
|
41
|
-
SimilarityLookup
|
42
|
-
].freeze
|
43
|
-
private_constant :OPERATIONS
|
44
|
-
|
45
|
-
params 'AssociateTag' => -> { associate_tag },
|
46
|
-
'Service' => 'AWSECommerceService',
|
47
|
-
'SubscriptionId' => -> { subscription_id },
|
48
|
-
'Version' => -> { version }
|
49
|
-
|
50
|
-
attr_accessor :associate_tag, :subscription_id
|
51
|
-
attr_writer :version
|
52
|
-
|
53
|
-
# Create a new request for given locale.
|
54
|
-
#
|
55
|
-
# locale - The String Product Advertising API locale (default: US).
|
56
|
-
# secure - Whether to use the secure version of the endpoint (default:
|
57
|
-
# false)
|
13
|
+
# @return [HTTP::Client]
|
14
|
+
attr_reader :client
|
15
|
+
|
16
|
+
# @return [Locale]
|
17
|
+
attr_reader :locale
|
18
|
+
|
19
|
+
# @return [Operation]
|
20
|
+
attr_reader :operation
|
21
|
+
|
22
|
+
# Creates a new request
|
58
23
|
#
|
59
|
-
#
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
24
|
+
# @overload initialize(marketplace: :us, access_key:, secret_key:, partner_tag:, partner_type:)
|
25
|
+
# @param [Symbol,String] marketplace
|
26
|
+
# @param [String] access_key
|
27
|
+
# @param [String] secret_key
|
28
|
+
# @param [String] partner_tag
|
29
|
+
# @param [String] partner_type
|
30
|
+
# @raise [Locale::NotFound] if marketplace is not found
|
31
|
+
def initialize(marketplace: :us, **args)
|
32
|
+
@locale = Locale.new(marketplace, **args)
|
33
|
+
@client = HTTP::Client.new
|
64
34
|
end
|
65
35
|
|
66
|
-
#
|
67
|
-
#
|
68
|
-
# credentials - The Hash credentials of the API endpoint.
|
69
|
-
# :aws_access_key_id - The String Amazon Web Services
|
70
|
-
# (AWS) key.
|
71
|
-
# :aws_secret_access_key - The String AWS secret.
|
72
|
-
# :associate_tag - The String Associate Tag.
|
73
|
-
# :aws_version - The String AWS version.
|
36
|
+
# Returns details about specified browse nodes
|
74
37
|
#
|
75
|
-
#
|
76
|
-
|
77
|
-
|
78
|
-
|
38
|
+
# @see https://webservices.amazon.com/paapi5/documentation/getbrowsenodes.html
|
39
|
+
# @overload get_browse_nodes(browse_node_ids:, languages_of_preference: nil, marketplace: nil, partner_tag: nil, partner_type: nil, resources: nil)
|
40
|
+
# @param [Array<String,Integer>,String,Integer] browse_node_ids
|
41
|
+
# @param [Array<String>,nil] languages_of_preference
|
42
|
+
# @param [String,nil] marketplace
|
43
|
+
# @param [String,nil] partner_tag
|
44
|
+
# @param [String,nil] partner_type
|
45
|
+
# @param [Array<String>,nil] resources
|
46
|
+
# @return [Response]
|
47
|
+
def get_browse_nodes(browse_node_ids:, **params)
|
48
|
+
params.update(browse_node_ids: Array(browse_node_ids))
|
49
|
+
request('GetBrowseNodes', params)
|
79
50
|
end
|
80
51
|
|
81
|
-
# Returns the
|
82
|
-
|
83
|
-
|
52
|
+
# Returns the attributes of one or more items
|
53
|
+
#
|
54
|
+
# @see https://webservices.amazon.com/paapi5/documentation/get-items.html
|
55
|
+
# @overload get_items(condition: nil, currency_of_preference: nil, item_id_type: nil, item_ids:, languages_of_preference: nil, marketplace: nil, merchant: nil, offer_count: nil, partner_tag: nil, partner_type: nil, resources: nil)
|
56
|
+
# @param [String,nil] condition
|
57
|
+
# @param [String,nil] currency_of_preference
|
58
|
+
# @param [String,nil] item_id_type
|
59
|
+
# @param [Array<String>,String] item_ids
|
60
|
+
# @param [Array<String>,nil] languages_of_preference
|
61
|
+
# @param [String,nil] marketplace
|
62
|
+
# @param [String,nil] merchant
|
63
|
+
# @param [Integer,nil] offer_count
|
64
|
+
# @param [String,nil] partner_tag
|
65
|
+
# @param [String,nil] partner_type
|
66
|
+
# @param [Array<String>,nil] resources
|
67
|
+
# @return [Response]
|
68
|
+
def get_items(item_ids:, **params)
|
69
|
+
params.update(item_ids: Array(item_ids))
|
70
|
+
request('GetItems', params)
|
84
71
|
end
|
85
72
|
|
86
|
-
#
|
87
|
-
#
|
73
|
+
# Returns a set of items that are the same product, but differ according to
|
74
|
+
# a consistent theme
|
88
75
|
#
|
89
|
-
#
|
90
|
-
#
|
76
|
+
# @see https://webservices.amazon.com/paapi5/documentation/get-variations.html
|
77
|
+
# @overload get_variations(asin:, condition: nil, currency_of_preference: nil, languages_of_preference: nil, marketplace: nil, merchant: nil, offer_count: nil, partner_tag: nil, partner_type: nil, resources: nil, variation_count: nil, variation_page: nil)
|
78
|
+
# @param [String] asin
|
79
|
+
# @param [String,nil] condition
|
80
|
+
# @param [String,nil] currency_of_preference
|
81
|
+
# @param [Array<String>,nil] languages_of_preference
|
82
|
+
# @param [String,nil] marketplace
|
83
|
+
# @param [String,nil] merchant
|
84
|
+
# @param [Integer,nil] offer_count
|
85
|
+
# @param [String,nil] partner_tag
|
86
|
+
# @param [String,nil] partner_type
|
87
|
+
# @param [Array<String>,nil] resources
|
88
|
+
# @param [Integer,nil] variation_count
|
89
|
+
# @param [Integer,nil] variation_page
|
90
|
+
# @return [Response]
|
91
|
+
def get_variations(**params)
|
92
|
+
request('GetVariations', params)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Searches for items on Amazon based on a search query
|
91
96
|
#
|
92
|
-
#
|
93
|
-
#
|
97
|
+
# @see https://webservices.amazon.com/paapi5/documentation/search-items.html
|
98
|
+
# @overload search_items(actor: nil, artist: nil, author: nil, availability: nil, brand: nil, browse_node_id: nil, condition: nil, currency_of_preference: nil, delivery_flags: nil, item_count: nil, item_page: nil, keywords: nil, languages_of_preference: nil, marketplace: nil, max_price: nil, merchant: nil, min_price: nil, min_reviews_rating: nil, min_savings_percent: nil, offer_count: nil, partner_tag: nil, partner_type: nil, resources: nil, search_index: nil, sort_by: nil, title: nil)
|
99
|
+
# @param [String,nil] actor
|
100
|
+
# @param [String,nil] artist
|
101
|
+
# @param [String,nil] availability
|
102
|
+
# @param [String,nil] brand
|
103
|
+
# @param [Integer,nil] browse_node_id
|
104
|
+
# @param [String,nil] condition
|
105
|
+
# @param [String,nil] currency_of_preference
|
106
|
+
# @param [Array<String>,nil] delivery_flags
|
107
|
+
# @param [Integer,nil] item_count
|
108
|
+
# @param [Integer,nil] item_page
|
109
|
+
# @param [String,nil] keywords
|
110
|
+
# @param [Array<String>,nil] languages_of_preference
|
111
|
+
# @param [Integer,nil] max_price
|
112
|
+
# @param [String,nil] merchant
|
113
|
+
# @param [Integer,nil] min_price
|
114
|
+
# @param [Integer,nil] min_reviews_rating
|
115
|
+
# @param [Integer,nil] min_savings_percent
|
116
|
+
# @param [Integer,nil] offer_count
|
117
|
+
# @param [Hash,nil] properties
|
118
|
+
# @param [Array<String>,nil] resources
|
119
|
+
# @param [String,nil] search_index
|
120
|
+
# @param [String,nil] sort_by
|
121
|
+
# @param [String,nil] title
|
122
|
+
# @return [Response]
|
123
|
+
def search_items(**params)
|
124
|
+
request('SearchItems', params)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Flags as persistent
|
94
128
|
#
|
95
|
-
#
|
129
|
+
# @param [Integer] timeout
|
130
|
+
# @return [self]
|
131
|
+
def persistent(timeout: 5)
|
132
|
+
host = "https://#{locale.host}"
|
133
|
+
@client = client.persistent(host, timeout: timeout)
|
134
|
+
|
135
|
+
self
|
136
|
+
end
|
137
|
+
|
138
|
+
# @!method use(*features)
|
139
|
+
# Turn on {https://github.com/httprb/http HTTP} features
|
96
140
|
#
|
97
|
-
#
|
98
|
-
#
|
99
|
-
# 'Keywords' => 'Architecture'
|
100
|
-
# )
|
141
|
+
# @param features
|
142
|
+
# @return [self]
|
101
143
|
#
|
102
|
-
#
|
103
|
-
#
|
104
|
-
# 'SearchIndex' => 'All',
|
105
|
-
# 'Keywords' => 'Architecture'
|
106
|
-
# },
|
107
|
-
# persistent: true
|
108
|
-
# )
|
144
|
+
# @!method via(*proxy)
|
145
|
+
# Make a request through an HTTP proxy
|
109
146
|
#
|
110
|
-
#
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
opts[:query].update('Operation' => operation)
|
117
|
-
|
118
|
-
Response.new(get(opts))
|
147
|
+
# @param [Array] proxy
|
148
|
+
# @raise [HTTP::Request::Error] if HTTP proxy is invalid
|
149
|
+
# @return [self]
|
150
|
+
%i[timeout via through headers use].each do |method_name|
|
151
|
+
define_method(method_name) do |*args, &block|
|
152
|
+
@client = client.send(method_name, *args, &block)
|
119
153
|
end
|
120
154
|
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def validate(params)
|
159
|
+
validate_keywords(params)
|
160
|
+
validate_resources(params)
|
161
|
+
end
|
162
|
+
|
163
|
+
def validate_keywords(params)
|
164
|
+
return unless params[:keywords]
|
165
|
+
return if params[:keywords].is_a?(String)
|
166
|
+
|
167
|
+
raise ArgumentError, ':keyword argument expects a String'
|
168
|
+
end
|
169
|
+
|
170
|
+
def validate_resources(params)
|
171
|
+
return unless params[:resources]
|
172
|
+
|
173
|
+
raise ArgumentError, ':resources argument expects an Array' unless params[:resources].is_a?(Array)
|
174
|
+
|
175
|
+
params[:resources].each do |resource|
|
176
|
+
raise ArgumentError, "There is not such resource: #{resource}" unless Resource.valid?(resource)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def request(operation_name, params)
|
181
|
+
validate(params)
|
182
|
+
@operation = Operation.new(operation_name, params: params, locale: locale)
|
183
|
+
response = client.headers(operation.headers)
|
184
|
+
.post(operation.url, body: operation.body)
|
185
|
+
|
186
|
+
Response.new(response)
|
187
|
+
end
|
121
188
|
end
|
122
189
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vacuum
|
4
|
+
# Resources determine what information will be returned in the API response
|
5
|
+
#
|
6
|
+
# @see https://webservices.amazon.com/paapi5/documentation/resources.html
|
7
|
+
class Resource
|
8
|
+
ALL = %w[BrowseNodeInfo.BrowseNodes
|
9
|
+
BrowseNodeInfo.BrowseNodes.Ancestor
|
10
|
+
BrowseNodeInfo.BrowseNodes.SalesRank
|
11
|
+
BrowseNodeInfo.WebsiteSalesRank
|
12
|
+
Images.Primary.Small
|
13
|
+
Images.Primary.Medium
|
14
|
+
Images.Primary.Large
|
15
|
+
Images.Variants.Small
|
16
|
+
Images.Variants.Medium
|
17
|
+
Images.Variants.Large
|
18
|
+
ItemInfo.ByLineInfo
|
19
|
+
ItemInfo.Classifications
|
20
|
+
ItemInfo.ContentInfo
|
21
|
+
ItemInfo.ContentRating
|
22
|
+
ItemInfo.ExternalIds
|
23
|
+
ItemInfo.Features
|
24
|
+
ItemInfo.ManufactureInfo
|
25
|
+
ItemInfo.ProductInfo
|
26
|
+
ItemInfo.TechnicalInfo
|
27
|
+
ItemInfo.Title
|
28
|
+
ItemInfo.TradeInInfo
|
29
|
+
Offers.Listings.Availability.MaxOrderQuantity
|
30
|
+
Offers.Listings.Availability.Message
|
31
|
+
Offers.Listings.Availability.MinOrderQuantity
|
32
|
+
Offers.Listings.Availability.Type
|
33
|
+
Offers.Listings.Condition
|
34
|
+
Offers.Listings.Condition.SubCondition
|
35
|
+
Offers.Listings.DeliveryInfo.IsAmazonFulfilled
|
36
|
+
Offers.Listings.DeliveryInfo.IsFreeShippingEligible
|
37
|
+
Offers.Listings.DeliveryInfo.IsPrimeEligible
|
38
|
+
Offers.Listings.IsBuyBoxWinner
|
39
|
+
Offers.Listings.LoyaltyPoints.Points
|
40
|
+
Offers.Listings.MerchantInfo
|
41
|
+
Offers.Listings.Price
|
42
|
+
Offers.Listings.ProgramEligibility.IsPrimeExclusive
|
43
|
+
Offers.Listings.ProgramEligibility.IsPrimePantry
|
44
|
+
Offers.Listings.Promotions
|
45
|
+
Offers.Listings.SavingBasis
|
46
|
+
Offers.Summaries.HighestPrice
|
47
|
+
Offers.Summaries.LowestPrice
|
48
|
+
Offers.Summaries.OfferCount
|
49
|
+
ParentASIN].freeze
|
50
|
+
private_constant :ALL
|
51
|
+
|
52
|
+
# @!attribute [r] all
|
53
|
+
# @return [Array<String>]
|
54
|
+
def self.all
|
55
|
+
ALL
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.valid?(resource)
|
59
|
+
ALL.include?(resource)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/vacuum/response.rb
CHANGED
@@ -2,35 +2,47 @@
|
|
2
2
|
|
3
3
|
require 'delegate'
|
4
4
|
require 'forwardable'
|
5
|
-
require '
|
5
|
+
require 'json'
|
6
6
|
|
7
7
|
module Vacuum
|
8
|
-
# A wrapper around the
|
8
|
+
# A wrapper around the API response
|
9
9
|
class Response < SimpleDelegator
|
10
10
|
extend Forwardable
|
11
11
|
|
12
|
+
# @!method dig(*key)
|
13
|
+
# Delegates to the Hash returned by {Response#to_h} to extract a nested
|
14
|
+
# value specified by the sequence of keys
|
15
|
+
#
|
16
|
+
# @param [String] key one or more keys
|
17
|
+
# @see https://ruby-doc.org/core/Hash.html#method-i-dig
|
18
|
+
def_delegator :to_h, :dig
|
19
|
+
|
12
20
|
class << self
|
21
|
+
# @return [nil,.parse] an optional custom parser
|
13
22
|
attr_accessor :parser
|
14
23
|
end
|
15
24
|
|
16
|
-
|
17
|
-
|
25
|
+
# @return [nil,.parse] an optional custom parser
|
18
26
|
attr_writer :parser
|
19
27
|
|
28
|
+
# @!attribute [r] parser
|
29
|
+
# @return [nil,.parse] an optional custom parser
|
20
30
|
def parser
|
21
31
|
@parser || self.class.parser
|
22
32
|
end
|
23
33
|
|
34
|
+
# Parses the response body
|
35
|
+
#
|
36
|
+
# @note Delegates to {#to_h} if no custom parser is set
|
24
37
|
def parse
|
25
38
|
parser ? parser.parse(body) : to_h
|
26
39
|
end
|
27
40
|
|
41
|
+
# Casts body to Hash
|
42
|
+
#
|
43
|
+
# @return [Hash]
|
28
44
|
def to_h
|
29
|
-
|
30
|
-
end
|
31
|
-
|
32
|
-
def body
|
33
|
-
(+__getobj__.body).force_encoding(Encoding::UTF_8)
|
45
|
+
JSON.parse(body)
|
34
46
|
end
|
35
47
|
end
|
36
48
|
end
|