vacuum 2.2.0 → 3.4.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.
- 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
|