vacuum 4.3.0 → 5.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f9defa7d4e94d0039667f94ede365982df2d9fac5a63be691a90e0efe3124aa1
4
- data.tar.gz: fc35f8f66b767c8f8a9951e83cd247611b00c221715ca633eb84916d6d9d294c
3
+ metadata.gz: dc789e68874474859f4ca103b08ce6c56e7d714eafa76f1f2780efabc232726f
4
+ data.tar.gz: 48d6f52b193a912da0ed88845813d6814786ea60fc83ceff2541407c482e4097
5
5
  SHA512:
6
- metadata.gz: e21de47a9ce2ab5fdfb290c7d429e884c9c41c09e19a276ae76a90b2157aa38f83e99538633f2c7cad26cdc85e595885f28654d6072dd308fd87cd793fb36633
7
- data.tar.gz: 619767b577fc2ecdcd04e4f034973a244d9b5f37319094df8fb58633c048c8644bce310fe5b16c0e27bb432ec7b7760b29689598b9417af3c37ca3c698d82685
6
+ metadata.gz: 7a58ce7a92532ac352ebef589caf42192a958af8203fdf4eae1e0d859b317b13afc8f9b79b635645b92d4bdb756eb9f21e4505ca5c2c091ab1235b32a2dc2dec
7
+ data.tar.gz: 538c6c125cf2d3b22622df7ff0ac91ffa76536edcf81ab3d8849968046f016926e64da300301a53fa5757125d16d96dbb67180539b62dc3476de8a248a0efec7
data/README.md CHANGED
@@ -1,14 +1,6 @@
1
1
  # Vacuum
2
2
 
3
- [![Build](https://github.com/hakanensari/vacuum/workflows/build/badge.svg)](https://github.com/hakanensari/vacuum/actions)
4
- [![Maintainability](https://api.codeclimate.com/v1/badges/ffbab4aa5fedf5780332/maintainability)](https://codeclimate.com/github/hakanensari/vacuum/maintainability)
5
- [![Test Coverage](https://api.codeclimate.com/v1/badges/ffbab4aa5fedf5780332/test_coverage)](https://codeclimate.com/github/hakanensari/vacuum/test_coverage)
6
-
7
- Vacuum is a Ruby wrapper to [Amazon Product Advertising API 5.0](https://webservices.amazon.com/paapi5/documentation/). The API provides programmatic access to query product information on the Amazon marketplaces.
8
-
9
- Cart Form functionality is not covered by this gem but is a primary focus on [carriage gem](https://github.com/skatkov/carriage)
10
-
11
- You need to [register first](https://webservices.amazon.com/paapi5/documentation/register-for-pa-api.html) to use the API.
3
+ Vacuum is a Ruby wrapper to the [Amazon Creators API](https://affiliate-program.amazon.com/creatorsapi/docs/en-us/). The API provides programmatic access to product information on the Amazon marketplaces.
12
4
 
13
5
  ![vacuum](https://github.com/hakanensari/vacuum/blob/main/images/vacuum.jpg?raw=true)
14
6
 
@@ -16,40 +8,53 @@ You need to [register first](https://webservices.amazon.com/paapi5/documentation
16
8
 
17
9
  ### Getting Started
18
10
 
19
- Create a request with your marketplace credentials. Set the marketplace by passing its two-letter country code.
11
+ Create a client with your credentials.
20
12
 
21
13
  ```ruby
22
- request = Vacuum.new(marketplace: 'US',
23
- access_key: '<ACCESS_KEY>',
24
- secret_key: '<SECRET_KEY>',
25
- partner_tag: '<PARTNER_TAG>')
14
+ client = Vacuum.new(
15
+ credential_id: "YOUR_CREDENTIAL_ID",
16
+ credential_secret: "YOUR_CREDENTIAL_SECRET",
17
+ version: "2.1"
18
+ )
26
19
  ```
27
20
 
21
+ The client handles authentication automatically and caches access tokens for one hour.
22
+
28
23
  You can now access the API using the available operations.
29
24
 
30
25
  ```ruby
31
- response = request.search_items(title: 'lean startup')
32
- puts response.to_h
26
+ response = client.search_items(
27
+ marketplace: "www.amazon.com",
28
+ partner_tag: "yourtag-20",
29
+ keywords: "lean startup"
30
+ )
31
+ response.parse
33
32
  ```
34
33
 
35
- Create a persistent connection to make multiple requests.
34
+ ### Versions and Marketplaces
36
35
 
37
- ```ruby
38
- request.persistent
39
- ```
36
+ The `version` determines which authentication endpoint to use:
37
+
38
+ | Version | Region |
39
+ |---------|--------|
40
+ | `"2.1"` | North America |
41
+ | `"2.2"` | Europe |
42
+ | `"2.3"` | Far East |
40
43
 
41
44
  ### Operations
42
45
 
43
- Refer to the [API docs](https://webservices.amazon.com/paapi5/documentation/) for more detailed information.
46
+ Each operation requires `marketplace` (e.g., `"www.amazon.com"`) and `partner_tag` parameters.
44
47
 
45
48
  #### GetBrowseNodes
46
49
 
47
- Given a BrowseNodeId, the `GetBrowseNodes` operation returns details about the specified browse node, like name, children and ancestors, depending on the resources specified in the request. The names and browse node IDs of the children and ancestor browse nodes are also returned. `GetBrowseNodes` enables you to traverse the browse node hierarchy to find a browse node.
50
+ Given a BrowseNodeId, the `GetBrowseNodes` operation returns details about the specified browse node, like name, children and ancestors, depending on the resources specified in the request.
48
51
 
49
52
  ```ruby
50
- request.get_browse_nodes(
51
- browse_node_ids: ['283155', '3040'],
52
- resources: ['BrowseNodes.Ancestor', 'BrowseNodes.Children']
53
+ client.get_browse_nodes(
54
+ marketplace: "www.amazon.com",
55
+ partner_tag: "yourtag-20",
56
+ browse_node_ids: ["283155", "3040"],
57
+ resources: ["browseNodes.ancestor", "browseNodes.children"]
53
58
  )
54
59
  ```
55
60
 
@@ -58,23 +63,27 @@ request.get_browse_nodes(
58
63
  Given an Item identifier, the `GetItems` operation returns the item attributes, based on the resources specified in the request.
59
64
 
60
65
  ```ruby
61
- request.get_items(
62
- item_ids: ['B0199980K4', 'B000HZD168', 'B01180YUXS', 'B00BKQTA4A'],
63
- resources: ['Images.Primary.Small', 'ItemInfo.Title', 'ItemInfo.Features',
64
- 'Offers.Summaries.HighestPrice' , 'ParentASIN']
66
+ client.get_items(
67
+ marketplace: "www.amazon.com",
68
+ partner_tag: "yourtag-20",
69
+ item_ids: ["B0199980K4", "B000HZD168"],
70
+ resources: ["images.primary.small", "itemInfo.title", "itemInfo.features",
71
+ "offersV2.listings.price", "parentASIN"]
65
72
  )
66
73
  ```
67
74
 
68
75
  #### GetVariations
69
76
 
70
- Given an ASIN, the `GetVariations` operation returns a set of items that are the same product, but differ according to a consistent theme, for example size and color. These items which differ according to a consistent theme are called variations. A variation is a child ASIN. The parent ASIN is an abstraction of the children items. For example, a shirt is a parent ASIN and parent ASINs cannot be sold. A child ASIN would be a blue shirt, size 16, sold by MyApparelStore. This child ASIN is one of potentially many variations. The ways in which variations differ are called dimensions.
77
+ Given an ASIN, the `GetVariations` operation returns a set of items that are the same product, but differ according to a consistent theme, for example size and color.
71
78
 
72
79
  ```ruby
73
- request.get_variations(
74
- asin: 'B00422MCUS',
75
- resources: ['ItemInfo.Title', 'VariationSummary.Price.HighestPrice',
76
- 'VariationSummary.Price.LowestPrice',
77
- 'VariationSummary.VariationDimension']
80
+ client.get_variations(
81
+ marketplace: "www.amazon.com",
82
+ partner_tag: "yourtag-20",
83
+ asin: "B00422MCUS",
84
+ resources: ["itemInfo.title", "variationSummary.price.highestPrice",
85
+ "variationSummary.price.lowestPrice",
86
+ "variationSummary.variationDimension"]
78
87
  )
79
88
  ```
80
89
 
@@ -83,65 +92,35 @@ request.get_variations(
83
92
  The `SearchItems` operation searches for items on Amazon based on a search query. The API returns up to ten items per search request.
84
93
 
85
94
  ```ruby
86
- request.search_items(keywords: 'harry potter')
95
+ client.search_items(
96
+ marketplace: "www.amazon.com",
97
+ partner_tag: "yourtag-20",
98
+ keywords: "harry potter"
99
+ )
87
100
  ```
88
101
 
89
102
  ### Response
90
103
 
91
- Consume a response by parsing it into a Ruby hash.
92
-
93
- ```ruby
94
- response.to_h
95
- ```
96
-
97
- You can also `#dig` into this hash.
98
-
99
- ```ruby
100
- response.dig('ItemsResult', 'Items')
101
- ```
102
-
103
- ### Logging
104
-
105
- Write requests and reponses to a logger using the logging feature of the [HTTP gem](https://github.com/httprb/http) under the hood:
106
-
107
- ```ruby
108
- require 'logger'
109
-
110
- logger = Logger.new(STDOUT)
111
- request.use(logging: {logger: logger})
112
- ```
113
-
114
- ### Bring your parser
115
- You can extend Vacuum with a custom parser. Just swap the original with a class or module that responds to `.parse`.
104
+ Parse the response body into a hash.
116
105
 
117
106
  ```ruby
118
- response.parser = MyParser
119
- response.parse
107
+ response.parse.dig("searchResult", "items")
120
108
  ```
121
109
 
122
- If no custom parser is set, `Vacuum::Response#parse` delegates to `#to_h`.
123
-
124
- ### VCR
110
+ ### Shared Token Cache
125
111
 
126
- If you are using [VCR](https://github.com/vcr/vcr) to test an app that accesses the API, you can use the custom VCR matcher of Vacuum to stub requests.
112
+ By default, each client instance caches its own access token. In multi-process environments like Rails, you can share the token across processes by providing a cache store.
127
113
 
128
114
  ```ruby
129
- require 'vacuum/matcher'
130
-
131
- # in your test
132
- VCR.insert_cassette('cassette_name',
133
- match_requests_on: [Vacuum::Matcher])
115
+ client = Vacuum.new(
116
+ credential_id: "YOUR_CREDENTIAL_ID",
117
+ credential_secret: "YOUR_CREDENTIAL_SECRET",
118
+ version: "2.1",
119
+ cache: Rails.cache
120
+ )
134
121
  ```
135
122
 
136
- In RSpec, use the `:paapi` metadata.
137
-
138
- ```ruby
139
- require 'vacuum/matcher'
140
-
141
- # in your test
142
- it 'queries Amazon', :paapi do
143
- end
144
- ```
123
+ The cache must respond to `fetch(key, expires_in:, &block)`.
145
124
 
146
125
  ## Development
147
126
 
@@ -151,30 +130,16 @@ Clone the repo and install dependencies.
151
130
  bundle install
152
131
  ```
153
132
 
154
- Tests and Rubocop should now pass as-is.
133
+ Run tests and Rubocop.
155
134
 
156
135
  ```sh
157
136
  bundle exec rake
158
137
  ```
159
138
 
160
- By default, the tests stub requests. Use the `RECORD` env var to record new interactions.
161
-
162
- ```sh
163
- RECORD=true bundle exec rake test
164
- ```
165
-
166
- You can set the `LIVE` env var to run all tests against live data.
167
-
168
- ```sh
169
- LIVE=true bundle exec rake test
170
- ```
171
-
172
- In either case, add actual API credentials to a [`locales.yml`](https://github.com/hakanensari/vacuum/blob/master/test/locales.yml.example) file under `test`.
173
-
174
- ## Getting Help
139
+ To run API tests, copy `spec/credentials.yml.example` to `spec/credentials.yml` and fill in your credentials.
175
140
 
176
- * Ask specific questions about the API on the [Amazon forum](https://forums.aws.amazon.com/forum.jspa?forumID=9).
177
- * Report bugs in [GitHub issues](https://github.com/hakanensari/vacuum/issues).
178
- * Discuss potential features in [GitHub Discussions](https://github.com/hakanensari/vacuum/discussions).
141
+ ---
179
142
 
143
+ > Amazon is a $250 billion dollar company that reacts to you buying a vacuum by going THIS GUY LOVES BUYING VACUUMS HERE ARE SOME MORE VACUUMS
180
144
 
145
+ — [@kibblesmith](https://web.archive.org/web/2017/https://twitter.com/kibblesmith/status/724817086309142529)
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "http"
4
+
5
+ module Vacuum
6
+ # A client for the Amazon Creators API
7
+ #
8
+ # Handles authentication automatically and caches access tokens.
9
+ #
10
+ # @example Basic usage
11
+ # client = Vacuum::Client.new(
12
+ # credential_id: "YOUR_CREDENTIAL_ID",
13
+ # credential_secret: "YOUR_CREDENTIAL_SECRET",
14
+ # version: "2.1"
15
+ # )
16
+ # response = client.search_items(
17
+ # marketplace: "www.amazon.com",
18
+ # partner_tag: "yourtag-20",
19
+ # keywords: "ruby programming"
20
+ # )
21
+ #
22
+ # @example With shared cache (for multi-process environments like Rails)
23
+ # client = Vacuum::Client.new(
24
+ # credential_id: "YOUR_CREDENTIAL_ID",
25
+ # credential_secret: "YOUR_CREDENTIAL_SECRET",
26
+ # version: "2.1",
27
+ # cache: Rails.cache
28
+ # )
29
+ class Client
30
+ AUTH_URLS = {
31
+ "2.1" => "https://creatorsapi.auth.us-east-1.amazoncognito.com/oauth2/token",
32
+ "2.2" => "https://creatorsapi.auth.eu-south-2.amazoncognito.com/oauth2/token",
33
+ "2.3" => "https://creatorsapi.auth.us-west-2.amazoncognito.com/oauth2/token",
34
+ }.freeze
35
+
36
+ API_URL = "https://creatorsapi.amazon/catalog/v1"
37
+
38
+ TOKEN_TTL = 3570 # 59.5 minutes (tokens valid for 1 hour)
39
+
40
+ # Creates a new client
41
+ #
42
+ # @param credential_id [String] Your Creators API credential ID
43
+ # @param credential_secret [String] Your Creators API credential secret
44
+ # @param version [String] API version ("2.1", "2.2", or "2.3")
45
+ # @param cache [#fetch] Optional cache store
46
+ # @param http [HTTP::Client]
47
+ def initialize(version:, credential_id:, credential_secret:, cache: nil, http: HTTP)
48
+ @version = version
49
+ @auth_url = AUTH_URLS.fetch(version) do
50
+ raise ArgumentError, "Unknown version: #{version}"
51
+ end
52
+ @credential_id = credential_id
53
+ @credential_secret = credential_secret
54
+ @cache = cache
55
+ @http = http
56
+ end
57
+
58
+ # Returns details about specified browse nodes
59
+ #
60
+ # @param marketplace [String] The marketplace (e.g., "www.amazon.com")
61
+ # @param partner_tag [String] Your partner/tracking tag
62
+ # @param browse_node_ids [Array<String>] The browse node IDs to look up
63
+ # @return [HTTP::Response]
64
+ def get_browse_nodes(marketplace:, partner_tag:, browse_node_ids:, **params)
65
+ params[:browse_node_ids] = Array(browse_node_ids)
66
+ request("getBrowseNodes", marketplace:, partner_tag:, **params)
67
+ end
68
+
69
+ # Returns the attributes of one or more items
70
+ #
71
+ # @param marketplace [String] The marketplace (e.g., "www.amazon.com")
72
+ # @param partner_tag [String] Your partner/tracking tag
73
+ # @param item_ids [Array<String>, String] The item IDs (ASINs) to look up
74
+ # @return [HTTP::Response]
75
+ def get_items(marketplace:, partner_tag:, item_ids:, **params)
76
+ params[:item_ids] = Array(item_ids)
77
+ request("getItems", marketplace:, partner_tag:, **params)
78
+ end
79
+
80
+ # Returns variations of a product
81
+ #
82
+ # @param marketplace [String] The marketplace (e.g., "www.amazon.com")
83
+ # @param partner_tag [String] Your partner/tracking tag
84
+ # @param asin [String] The ASIN to get variations for
85
+ # @return [HTTP::Response]
86
+ def get_variations(marketplace:, partner_tag:, **params)
87
+ request("getVariations", marketplace:, partner_tag:, **params)
88
+ end
89
+
90
+ # Searches for items on Amazon
91
+ #
92
+ # @param marketplace [String] The marketplace (e.g., "www.amazon.com")
93
+ # @param partner_tag [String] Your partner/tracking tag
94
+ # @param keywords [String] Search keywords
95
+ # @return [HTTP::Response]
96
+ def search_items(marketplace:, partner_tag:, **params)
97
+ request("searchItems", marketplace:, partner_tag:, **params)
98
+ end
99
+
100
+ private
101
+
102
+ def request(operation, marketplace:, partner_tag:, **params)
103
+ @http
104
+ .headers(
105
+ "Authorization" => "Bearer #{access_token}, Version #{@version}",
106
+ "x-marketplace" => marketplace,
107
+ )
108
+ .post("#{API_URL}/#{operation}", json: camelize_keys(marketplace:, partner_tag:, **params))
109
+ end
110
+
111
+ def camelize_keys(**params)
112
+ params.transform_keys { |key| key.to_s.gsub(/_([a-z])/) { Regexp.last_match(1).upcase } }
113
+ end
114
+
115
+ def access_token
116
+ if @cache
117
+ @cache.fetch(cache_key, expires_in: TOKEN_TTL) { fetch_token }
118
+ else
119
+ @access_token = fetch_token if token_expired?
120
+ @access_token
121
+ end
122
+ end
123
+
124
+ def cache_key
125
+ "vacuum/#{@credential_id}/token"
126
+ end
127
+
128
+ def token_expired?
129
+ @access_token.nil? || Time.now >= @token_expires_at
130
+ end
131
+
132
+ def fetch_token
133
+ response = @http
134
+ .basic_auth(user: @credential_id, pass: @credential_secret)
135
+ .post(@auth_url, form: {
136
+ grant_type: "client_credentials",
137
+ scope: "creatorsapi/default",
138
+ })
139
+
140
+ data = response.parse
141
+ @token_expires_at = Time.now + TOKEN_TTL
142
+ @access_token = data["access_token"]
143
+ end
144
+ end
145
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vacuum
4
- VERSION = '4.3.0'
4
+ VERSION = "5.0.0"
5
5
  end
data/lib/vacuum.rb CHANGED
@@ -1,20 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
3
+ require "forwardable"
4
4
 
5
- require 'vacuum/request'
6
- require 'vacuum/version'
5
+ require "vacuum/client"
6
+ require "vacuum/version"
7
7
 
8
- # Ruby wrapper to the Amazon Product Advertising API
8
+ # Ruby wrapper to the Amazon Creators API
9
9
  module Vacuum
10
10
  class << self
11
11
  extend Forwardable
12
12
 
13
13
  # @!method new
14
- # Delegates to {Request} to create a new request
14
+ # Delegates to {Client} to create a new client
15
15
  #
16
- # @return [Request]
17
- # @see Request#initialize
18
- def_delegator 'Vacuum::Request', :new
16
+ # @return [Client]
17
+ # @see Client#initialize
18
+ def_delegator "Vacuum::Client", :new
19
19
  end
20
20
  end
metadata CHANGED
@@ -1,50 +1,30 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vacuum
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hakan Ensari
8
8
  - Stanislav Katkov
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-15 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: aws-sigv4
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: http
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - ">="
32
18
  - !ruby/object:Gem::Version
33
- version: '4.0'
34
- - - "<"
35
- - !ruby/object:Gem::Version
36
- version: '6.0'
19
+ version: '5.0'
37
20
  type: :runtime
38
21
  prerelease: false
39
22
  version_requirements: !ruby/object:Gem::Requirement
40
23
  requirements:
41
24
  - - ">="
42
25
  - !ruby/object:Gem::Version
43
- version: '4.0'
44
- - - "<"
45
- - !ruby/object:Gem::Version
46
- version: '6.0'
47
- description: A wrapper to the Amazon Product Advertising API
26
+ version: '5.0'
27
+ description: A wrapper to the Amazon Creators API
48
28
  email:
49
29
  - hakanensari@gmail.com
50
30
  - sk@skylup.com
@@ -55,11 +35,7 @@ files:
55
35
  - LICENSE
56
36
  - README.md
57
37
  - lib/vacuum.rb
58
- - lib/vacuum/locale.rb
59
- - lib/vacuum/matcher.rb
60
- - lib/vacuum/operation.rb
61
- - lib/vacuum/request.rb
62
- - lib/vacuum/response.rb
38
+ - lib/vacuum/client.rb
63
39
  - lib/vacuum/version.rb
64
40
  homepage: https://github.com/hakanensari/vacuum
65
41
  licenses:
@@ -80,7 +56,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
80
56
  - !ruby/object:Gem::Version
81
57
  version: '0'
82
58
  requirements: []
83
- rubygems_version: 3.6.2
59
+ rubygems_version: 4.0.3
84
60
  specification_version: 4
85
- summary: Amazon Product Advertising in Ruby
61
+ summary: Amazon Creators API in Ruby
86
62
  test_files: []
data/lib/vacuum/locale.rb DELETED
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vacuum
4
- # The target locale
5
- #
6
- # @see https://webservices.amazon.com/paapi5/documentation/common-request-parameters.html#host-and-region
7
- class Locale
8
- # Raised when the provided marketplace does not correspond to an existing
9
- # Amazon locale
10
- class NotFound < KeyError; end
11
-
12
- HOSTS_AND_REGIONS = {
13
- au: ['webservices.amazon.com.au', 'us-west-2'],
14
- be: ['webservices.amazon.be', 'eu-west-1'],
15
- br: ['webservices.amazon.com.br', 'us-east-1'],
16
- ca: ['webservices.amazon.ca', 'us-east-1'],
17
- eg: ['webservices.amazon.eg', 'eu-west-1'],
18
- fr: ['webservices.amazon.fr', 'eu-west-1'],
19
- de: ['webservices.amazon.de', 'eu-west-1'],
20
- in: ['webservices.amazon.in', 'eu-west-1'],
21
- ie: ['webservices.amazon.ie', 'eu-west-1'],
22
- it: ['webservices.amazon.it', 'eu-west-1'],
23
- jp: ['webservices.amazon.co.jp', 'us-west-2'],
24
- mx: ['webservices.amazon.com.mx', 'us-east-1'],
25
- nl: ['webservices.amazon.nl', 'eu-west-1'],
26
- pl: ['webservices.amazon.pl', 'eu-west-1'],
27
- sg: ['webservices.amazon.sg', 'us-west-2'],
28
- sa: ['webservices.amazon.sa', 'eu-west-1'],
29
- es: ['webservices.amazon.es', 'eu-west-1'],
30
- se: ['webservices.amazon.se', 'eu-west-1'],
31
- tr: ['webservices.amazon.com.tr', 'eu-west-1'],
32
- ae: ['webservices.amazon.ae', 'eu-west-1'],
33
- gb: ['webservices.amazon.co.uk', 'eu-west-1'],
34
- us: ['webservices.amazon.com', 'us-east-1']
35
- }.freeze
36
- private_constant :HOSTS_AND_REGIONS
37
-
38
- # @return [String]
39
- attr_reader :host, :region, :access_key, :secret_key, :partner_tag,
40
- :partner_type
41
-
42
- # Creates a locale
43
- #
44
- # @param [Symbol,String] marketplace
45
- # @param [String] access_key
46
- # @param [String] secret_key
47
- # @param [String] partner_tag
48
- # @param [String] partner_type
49
- # @raise [NotFound] if marketplace is not found
50
- def initialize(marketplace, access_key:, secret_key:, partner_tag:,
51
- partner_type: 'Associates')
52
- @host, @region = find_host_and_region(marketplace)
53
- @access_key = access_key
54
- @secret_key = secret_key
55
- @partner_tag = partner_tag
56
- @partner_type = partner_type
57
- end
58
-
59
- private
60
-
61
- def find_host_and_region(marketplace)
62
- marketplace = marketplace.to_sym.downcase
63
- marketplace = :gb if marketplace == :uk
64
-
65
- HOSTS_AND_REGIONS.fetch(marketplace)
66
- rescue KeyError
67
- raise NotFound, "marketplace not found: :#{marketplace}"
68
- end
69
- end
70
- end
@@ -1,73 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
-
5
- module Vacuum
6
- # Custom VCR matcher for stubbing calls to the Product Advertising API
7
- #
8
- # The matcher is not required by default.
9
- #
10
- # @example
11
- # require 'vacuum/matcher'
12
- #
13
- # # in your test
14
- # VCR.insert_cassette('cassette_name',
15
- # match_requests_on: [Vacuum::Matcher])
16
- #
17
- # @see https://relishapp.com/vcr/vcr/v/5-0-0/docs/request-matching/register-and-use-a-custom-matcher
18
- class Matcher
19
- IGNORED_KEYS = %w[PartnerTag].freeze
20
- private_constant :IGNORED_KEYS
21
-
22
- # @!visibility private
23
- attr_reader :requests
24
-
25
- # @!visibility private
26
- def self.call(*requests)
27
- new(*requests).compare
28
- end
29
-
30
- # @!visibility private
31
- def initialize(*requests)
32
- @requests = requests
33
- end
34
-
35
- # @!visibility private
36
- def compare
37
- uris.reduce(:==) && bodies.reduce(:==)
38
- end
39
-
40
- private
41
-
42
- def uris
43
- requests.map(&:uri)
44
- end
45
-
46
- def bodies
47
- requests.map do |req|
48
- params = JSON.parse(req.body)
49
- IGNORED_KEYS.each { |k| params.delete(k) }
50
-
51
- params
52
- end
53
- end
54
- end
55
- end
56
-
57
- # :nocov:
58
- if defined?(RSpec)
59
- RSpec.configure do |config|
60
- config.around do |example|
61
- if example.metadata[:paapi]
62
- metadata = example.metadata[:paapi]
63
- metadata = {} if metadata == true
64
- example.metadata[:vcr] = metadata.merge(
65
- match_requests_on: [Vacuum::Matcher]
66
- )
67
- end
68
-
69
- example.run
70
- end
71
- end
72
- end
73
- # :nocov:
@@ -1,75 +0,0 @@
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:, 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,173 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'http'
4
-
5
- require 'vacuum/locale'
6
- require 'vacuum/operation'
7
- require 'vacuum/response'
8
-
9
- module Vacuum
10
- # A request to the Amazon Product Advertising API
11
- class Request
12
- # @return [HTTP::Client]
13
- attr_reader :client
14
-
15
- # @return [Locale]
16
- attr_reader :locale
17
-
18
- # @return [Operation]
19
- attr_reader :operation
20
-
21
- # Creates a new request
22
- #
23
- # @overload initialize(marketplace: :us, access_key:, secret_key:, partner_tag:, partner_type:)
24
- # @param [Symbol,String] marketplace
25
- # @param [String] access_key
26
- # @param [String] secret_key
27
- # @param [String] partner_tag
28
- # @param [String] partner_type
29
- # @raise [Locale::NotFound] if marketplace is not found
30
- def initialize(marketplace: :us, **args)
31
- @locale = Locale.new(marketplace, **args)
32
- @client = HTTP::Client.new
33
- end
34
-
35
- # Returns details about specified browse nodes
36
- #
37
- # @see https://webservices.amazon.com/paapi5/documentation/getbrowsenodes.html
38
- # @overload get_browse_nodes(browse_node_ids:, languages_of_preference: nil, marketplace: nil, partner_tag: nil, partner_type: nil, resources: nil)
39
- # @param [Array<String,Integer>,String,Integer] browse_node_ids
40
- # @param [Array<String>,nil] languages_of_preference
41
- # @param [String,nil] marketplace
42
- # @param [String,nil] partner_tag
43
- # @param [String,nil] partner_type
44
- # @param [Array<String>,nil] resources
45
- # @return [Response]
46
- def get_browse_nodes(browse_node_ids:, **params)
47
- params.update(browse_node_ids: Array(browse_node_ids))
48
- request('GetBrowseNodes', params)
49
- end
50
-
51
- # Returns the attributes of one or more items
52
- #
53
- # @see https://webservices.amazon.com/paapi5/documentation/get-items.html
54
- # @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)
55
- # @param [String,nil] condition
56
- # @param [String,nil] currency_of_preference
57
- # @param [String,nil] item_id_type
58
- # @param [Array<String>,String] item_ids
59
- # @param [Array<String>,nil] languages_of_preference
60
- # @param [String,nil] marketplace
61
- # @param [String,nil] merchant
62
- # @param [Integer,nil] offer_count
63
- # @param [String,nil] partner_tag
64
- # @param [String,nil] partner_type
65
- # @param [Array<String>,nil] resources
66
- # @return [Response]
67
- def get_items(item_ids:, **params)
68
- params.update(item_ids: Array(item_ids))
69
- request('GetItems', params)
70
- end
71
-
72
- # Returns a set of items that are the same product, but differ according to
73
- # a consistent theme
74
- #
75
- # @see https://webservices.amazon.com/paapi5/documentation/get-variations.html
76
- # @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)
77
- # @param [String] asin
78
- # @param [String,nil] condition
79
- # @param [String,nil] currency_of_preference
80
- # @param [Array<String>,nil] languages_of_preference
81
- # @param [String,nil] marketplace
82
- # @param [String,nil] merchant
83
- # @param [Integer,nil] offer_count
84
- # @param [String,nil] partner_tag
85
- # @param [String,nil] partner_type
86
- # @param [Array<String>,nil] resources
87
- # @param [Integer,nil] variation_count
88
- # @param [Integer,nil] variation_page
89
- # @return [Response]
90
- def get_variations(**params)
91
- request('GetVariations', params)
92
- end
93
-
94
- # Searches for items on Amazon based on a search query
95
- #
96
- # @see https://webservices.amazon.com/paapi5/documentation/search-items.html
97
- # @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)
98
- # @param [String,nil] actor
99
- # @param [String,nil] artist
100
- # @param [String,nil] availability
101
- # @param [String,nil] brand
102
- # @param [Integer,nil] browse_node_id
103
- # @param [String,nil] condition
104
- # @param [String,nil] currency_of_preference
105
- # @param [Array<String>,nil] delivery_flags
106
- # @param [Integer,nil] item_count
107
- # @param [Integer,nil] item_page
108
- # @param [String,nil] keywords
109
- # @param [Array<String>,nil] languages_of_preference
110
- # @param [Integer,nil] max_price
111
- # @param [String,nil] merchant
112
- # @param [Integer,nil] min_price
113
- # @param [Integer,nil] min_reviews_rating
114
- # @param [Integer,nil] min_savings_percent
115
- # @param [Integer,nil] offer_count
116
- # @param [Hash,nil] properties
117
- # @param [Array<String>,nil] resources
118
- # @param [String,nil] search_index
119
- # @param [String,nil] sort_by
120
- # @param [String,nil] title
121
- # @return [Response]
122
- def search_items(**params)
123
- request('SearchItems', params)
124
- end
125
-
126
- # Flags as persistent
127
- #
128
- # @param [Integer] timeout
129
- # @return [self]
130
- def persistent(timeout: 5)
131
- host = "https://#{locale.host}"
132
- @client = client.persistent(host, timeout:)
133
-
134
- self
135
- end
136
-
137
- # @!method use(*features)
138
- # Turn on {https://github.com/httprb/http HTTP} features
139
- #
140
- # @param features
141
- # @return [self]
142
- #
143
- # @!method via(*proxy)
144
- # Make a request through an HTTP proxy
145
- #
146
- # @param [Array] proxy
147
- # @raise [HTTP::Request::Error] if HTTP proxy is invalid
148
- # @return [self]
149
- %i[timeout via through headers use].each do |method_name|
150
- define_method(method_name) do |*args, &block|
151
- @client = client.send(method_name, *args, &block)
152
- end
153
- end
154
-
155
- private
156
-
157
- def validate_keywords(params)
158
- return unless params[:keywords]
159
- return if params[:keywords].is_a?(String)
160
-
161
- raise ArgumentError, ':keyword argument expects a String'
162
- end
163
-
164
- def request(operation_name, params)
165
- validate_keywords(params)
166
- @operation = Operation.new(operation_name, params:, locale:)
167
- response = client.headers(operation.headers)
168
- .post(operation.url, body: operation.body)
169
-
170
- Response.new(response)
171
- end
172
- end
173
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'delegate'
4
- require 'forwardable'
5
- require 'json'
6
-
7
- module Vacuum
8
- # A wrapper around the API response
9
- class Response < SimpleDelegator
10
- extend Forwardable
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
-
20
- class << self
21
- # @return [nil,.parse] an optional custom parser
22
- attr_accessor :parser
23
- end
24
-
25
- # @return [nil,.parse] an optional custom parser
26
- attr_writer :parser
27
-
28
- # @!attribute [r] parser
29
- # @return [nil,.parse] an optional custom parser
30
- def parser
31
- @parser || self.class.parser
32
- end
33
-
34
- # Parses the response body
35
- #
36
- # @note Delegates to {#to_h} if no custom parser is set
37
- def parse
38
- parser&.parse(body) || to_h
39
- end
40
-
41
- # Casts body to Hash
42
- #
43
- # @return [Hash]
44
- def to_h
45
- JSON.parse(body)
46
- end
47
- end
48
- end