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.
@@ -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
@@ -1,122 +1,189 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'jeff'
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
- # An Amazon Product Advertising API request.
11
+ # A request to the Amazon Product Advertising API
8
12
  class Request
9
- include Jeff
10
-
11
- BadLocale = Class.new(ArgumentError)
12
-
13
- LATEST_VERSION = '2013-08-01'
14
-
15
- HOSTS = {
16
- 'AU' => 'webservices.amazon.com.au',
17
- 'BR' => 'webservices.amazon.com.br',
18
- 'CA' => 'webservices.amazon.ca',
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
- # Raises a Bad Locale error if locale is not valid.
60
- def initialize(locale = 'US', secure = false)
61
- locale = 'GB' if locale == 'UK'
62
- host = HOSTS.fetch(locale) { raise BadLocale }
63
- @aws_endpoint = "#{secure ? 'https' : 'http'}://#{host}/onca/xml"
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
- # Configure the Amazon Product Advertising API request.
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
- # Returns self.
76
- def configure(credentials)
77
- credentials.each { |key, val| send("#{key}=", val) }
78
- self
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 API version.
82
- def version
83
- @version || LATEST_VERSION
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
- # Execute an API operation. See `OPERATIONS` constant above for available
87
- # operation names.
73
+ # Returns a set of items that are the same product, but differ according to
74
+ # a consistent theme
88
75
  #
89
- # params - The Hash request parameters.
90
- # opts - Options passed to Excon (default: {}).
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
- # Alternatively, pass Excon options as first argument and include request
93
- # parameters as query key.
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
- # Examples
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
- # req.item_search(
98
- # 'SearchIndex' => 'All',
99
- # 'Keywords' => 'Architecture'
100
- # )
141
+ # @param features
142
+ # @return [self]
101
143
  #
102
- # req.item_search(
103
- # query: {
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
- # Returns a Vacuum Response.
111
- OPERATIONS.each do |operation|
112
- method_name = operation.gsub(/(.)([A-Z])/, '\1_\2').downcase
113
- define_method(method_name) do |params, opts = {}|
114
- params.key?(:query) ? opts = params : opts.update(query: params)
115
- opts[:expects] ||= [200, 400, 403]
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
@@ -2,35 +2,47 @@
2
2
 
3
3
  require 'delegate'
4
4
  require 'forwardable'
5
- require 'multi_xml'
5
+ require 'json'
6
6
 
7
7
  module Vacuum
8
- # A wrapper around the Amazon Product Advertising API response.
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
- def_delegator :to_h, :dig
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
- MultiXml.parse(body)
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