vacuum 3.0.0 → 3.1.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 +58 -24
- data/lib/vacuum.rb +5 -0
- data/lib/vacuum/locale.rb +51 -42
- data/lib/vacuum/matcher.rb +16 -1
- data/lib/vacuum/operation.rb +75 -0
- data/lib/vacuum/request.rb +32 -79
- data/lib/vacuum/response.rb +1 -1
- data/lib/vacuum/version.rb +1 -1
- data/test/cassettes/vacuum.yml +834 -253
- data/test/integration_helper.rb +0 -2
- data/test/locales.yml +16 -16
- data/test/locales.yml.example +8 -0
- data/test/vacuum/test_locale.rb +22 -14
- data/test/vacuum/test_operation.rb +31 -0
- data/test/vacuum/test_request.rb +40 -7
- metadata +8 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 93d495272c66aa409a35266354596c893f93dc94f909290143909a3ed3552f8a
|
4
|
+
data.tar.gz: '0812a94c0f3254302468caa19b852e31f7e7e4650166721ed45d49f071db5303'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 473700d6b71e5c6c3228f64fa2e6b78c4efc7d191211a686fb14ec8426f59fa68a6c626c742e1282cd08db8df9e6a0e6ce30b3f43b991908d96fe179cc8c37fd
|
7
|
+
data.tar.gz: 43f66d7d7a0dc877a9e197006d5aa869e14b699f86554181e79794ec2bbe92846e9a2f73b320228f5c552eb8d9e278736284d947a08995e358162f444e299dbc
|
data/README.md
CHANGED
@@ -1,20 +1,18 @@
|
|
1
1
|
# Vacuum
|
2
2
|
|
3
|
-
[![
|
3
|
+
[![Build](https://github.com/hakanensari/vacuum/workflows/build/badge.svg)](https://github.com/hakanensari/vacuum/actions)
|
4
4
|
|
5
|
-
Vacuum is a
|
5
|
+
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.
|
6
|
+
|
7
|
+
You need to [register first](https://webservices.amazon.com/paapi5/documentation/register-for-pa-api.html) to use the API.
|
6
8
|
|
7
9
|
![vacuum](http://f.cl.ly/items/2k2X0e2u0G3k1c260D2u/vacuum.png)
|
8
10
|
|
9
11
|
## Usage
|
10
12
|
|
11
|
-
Vacuum follows the nomenclature of the Product Advertising API. The examples below are based on examples in the [Amazon docs](https://webservices.amazon.com/paapi5/documentation/).
|
12
|
-
|
13
13
|
### Getting Started
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
Create a request with your marketplace credentials, passing the two-letter country code of the marketplace.
|
15
|
+
Create a request with your marketplace credentials. Set the marketplace by passing its two-letter country code.
|
18
16
|
|
19
17
|
```ruby
|
20
18
|
request = Vacuum.new(marketplace: 'US',
|
@@ -23,17 +21,26 @@ request = Vacuum.new(marketplace: 'US',
|
|
23
21
|
partner_tag: '<PARTNER_TAG>')
|
24
22
|
```
|
25
23
|
|
26
|
-
|
24
|
+
You can now access the API using the available operations.
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
response = request.search_items(title: 'lean startup')
|
28
|
+
puts response.to_h
|
29
|
+
```
|
30
|
+
|
31
|
+
Create a persistent connection to make multiple requests.
|
27
32
|
|
28
33
|
```ruby
|
29
|
-
|
34
|
+
request.persistent
|
30
35
|
```
|
31
36
|
|
32
37
|
### Operations
|
33
38
|
|
39
|
+
Refer to the [API docs](https://webservices.amazon.com/paapi5/documentation/) for more detailed information.
|
40
|
+
|
34
41
|
#### GetBrowseNodes
|
35
42
|
|
36
|
-
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.
|
43
|
+
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.
|
37
44
|
|
38
45
|
```ruby
|
39
46
|
request.get_browse_nodes(
|
@@ -69,7 +76,7 @@ request.get_variations(
|
|
69
76
|
|
70
77
|
#### SearchItems
|
71
78
|
|
72
|
-
The `SearchItems` operation searches for items on Amazon based on a search query. The
|
79
|
+
The `SearchItems` operation searches for items on Amazon based on a search query. The API returns up to ten items per search request.
|
73
80
|
|
74
81
|
```ruby
|
75
82
|
request.search_items(keywords: 'harry potter')
|
@@ -77,46 +84,73 @@ request.search_items(keywords: 'harry potter')
|
|
77
84
|
|
78
85
|
### Response
|
79
86
|
|
80
|
-
|
87
|
+
Consume a response by parsing it into a Ruby hash.
|
81
88
|
|
82
89
|
```ruby
|
83
90
|
response.to_h
|
84
91
|
```
|
85
92
|
|
86
|
-
You can also `#dig` into
|
93
|
+
You can also `#dig` into this hash.
|
87
94
|
|
88
95
|
```ruby
|
89
96
|
response.dig('ItemsResult', 'Items')
|
90
97
|
```
|
91
98
|
|
92
|
-
|
99
|
+
### Troubleshooting
|
100
|
+
|
101
|
+
In addition to the response payload, the following attributes help you introspect a request.
|
93
102
|
|
94
103
|
```ruby
|
95
|
-
|
96
|
-
|
104
|
+
request.body
|
105
|
+
request.headers
|
106
|
+
request.url
|
97
107
|
```
|
98
108
|
|
99
|
-
|
109
|
+
### VCR
|
100
110
|
|
101
|
-
|
111
|
+
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
|
+
|
113
|
+
```ruby
|
114
|
+
require 'vacuum/matcher'
|
115
|
+
|
116
|
+
# in your test
|
117
|
+
VCR.insert_cassette('cassette_name',
|
118
|
+
match_requests_on: [Vacuum::Matcher])
|
119
|
+
```
|
102
120
|
|
103
|
-
|
121
|
+
In RSpec, consider using custom metadata.
|
104
122
|
|
105
123
|
```ruby
|
106
124
|
require 'vacuum/matcher'
|
107
125
|
|
108
|
-
|
126
|
+
RSpec.configure do |config|
|
127
|
+
config.around do |example|
|
128
|
+
if example.metadata[:paapi]
|
129
|
+
metadata = example.metadata[:paapi]
|
130
|
+
metadata = {} if metadata == true
|
131
|
+
example.metadata[:vcr] = metadata.merge(
|
132
|
+
match_requests_on: [Vacuum::Matcher]
|
133
|
+
)
|
134
|
+
end
|
135
|
+
|
136
|
+
example.run
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# in your test
|
141
|
+
it 'queries Amazon', :paapi do
|
142
|
+
end
|
109
143
|
```
|
110
144
|
|
111
|
-
##
|
145
|
+
## Development
|
112
146
|
|
113
|
-
Tests should pass as-is
|
147
|
+
Clone the repo and install dependencies. Tests should pass as-is.
|
114
148
|
|
115
149
|
```sh
|
116
150
|
bundle exec rake
|
117
151
|
```
|
118
152
|
|
119
|
-
By default, all requests are stubbed. Use the `RECORD` env var to record new
|
153
|
+
By default, all requests are stubbed. Use the `RECORD` env var to record new interactions.
|
120
154
|
|
121
155
|
```sh
|
122
156
|
bundle exec RECORD=true rake
|
@@ -128,7 +162,7 @@ You can also run tests against live data.
|
|
128
162
|
bundle exec LIVE=true rake
|
129
163
|
```
|
130
164
|
|
131
|
-
In either case,
|
165
|
+
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`.
|
132
166
|
|
133
167
|
## Getting Help
|
134
168
|
|
data/lib/vacuum.rb
CHANGED
data/lib/vacuum/locale.rb
CHANGED
@@ -1,53 +1,62 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Vacuum
|
4
|
-
# The target
|
5
|
-
#
|
4
|
+
# The target locale
|
5
|
+
#
|
6
|
+
# @see https://webservices.amazon.com/paapi5/documentation/common-request-parameters.html#host-and-region
|
6
7
|
class Locale
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
8
|
+
# Raised when the provided marketplace does not correspond to an existing
|
9
|
+
# Amazon locale
|
10
|
+
class NotFound < KeyError; end
|
11
|
+
|
12
|
+
# @!visibility private
|
13
|
+
HOSTS_AND_REGIONS = {
|
14
|
+
au: ['webservices.amazon.com.au', 'us-west-2'],
|
15
|
+
br: ['webservices.amazon.com.br', 'us-east-1'],
|
16
|
+
ca: ['webservices.amazon.ca', 'us-east-1'],
|
17
|
+
fr: ['webservices.amazon.fr', 'eu-west-1'],
|
18
|
+
de: ['webservices.amazon.de', 'eu-west-1'],
|
19
|
+
in: ['webservices.amazon.in', 'eu-west-1'],
|
20
|
+
it: ['webservices.amazon.it', 'eu-west-1'],
|
21
|
+
jp: ['webservices.amazon.co.jp', 'us-west-2'],
|
22
|
+
mx: ['webservices.amazon.com.mx', 'us-east-1'],
|
23
|
+
es: ['webservices.amazon.es', 'eu-west-1'],
|
24
|
+
tr: ['webservices.amazon.com.tr', 'eu-west-1'],
|
25
|
+
ae: ['webservices.amazon.ae', 'eu-west-1'],
|
26
|
+
gb: ['webservices.amazon.co.uk', 'eu-west-1'],
|
27
|
+
us: ['webservices.amazon.com', 'us-east-1']
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
# @return [String]
|
31
|
+
attr_reader :host, :region, :access_key, :secret_key, :partner_tag,
|
32
|
+
:partner_type
|
33
|
+
|
34
|
+
# Creates a locale
|
35
|
+
#
|
36
|
+
# @param [Symbol,String] marketplace
|
37
|
+
# @param [String] access_key
|
38
|
+
# @param [String] secret_key
|
39
|
+
# @param [String] partner_tag
|
40
|
+
# @param [String] partner_type
|
41
|
+
# @raise [NotFound] if marketplace is not found
|
42
|
+
def initialize(marketplace, access_key:, secret_key:, partner_tag:,
|
43
|
+
partner_type: 'Associates')
|
44
|
+
@host, @region = find_host_and_region(marketplace)
|
45
|
+
@access_key = access_key
|
46
|
+
@secret_key = secret_key
|
47
|
+
@partner_tag = partner_tag
|
48
|
+
@partner_type = partner_type
|
22
49
|
end
|
23
50
|
|
24
|
-
|
25
|
-
"webservices.#{domain}"
|
26
|
-
end
|
51
|
+
private
|
27
52
|
|
28
|
-
def marketplace
|
29
|
-
|
30
|
-
|
53
|
+
def find_host_and_region(marketplace)
|
54
|
+
marketplace = marketplace.to_sym.downcase
|
55
|
+
marketplace = :gb if marketplace == :uk
|
31
56
|
|
32
|
-
|
33
|
-
|
57
|
+
HOSTS_AND_REGIONS.fetch(marketplace)
|
58
|
+
rescue KeyError
|
59
|
+
raise NotFound, "marketplace not found: :#{marketplace}"
|
34
60
|
end
|
35
|
-
|
36
|
-
@all = [
|
37
|
-
[:au, 'amazon.com.au', 'us-west-2'],
|
38
|
-
[:br, 'amazon.com.br', 'us-east-1'],
|
39
|
-
[:ca, 'amazon.ca', 'us-east-1'],
|
40
|
-
[:fr, 'amazon.fr', 'eu-west-1'],
|
41
|
-
[:de, 'amazon.de', 'eu-west-1'],
|
42
|
-
[:in, 'amazon.in', 'eu-west-1'],
|
43
|
-
[:it, 'amazon.it', 'eu-west-1'],
|
44
|
-
[:jp, 'amazon.co.jp', 'us-west-2'],
|
45
|
-
[:mx, 'amazon.com.mx', 'us-east-1'],
|
46
|
-
[:es, 'amazon.es', 'eu-west-1'],
|
47
|
-
[:tr, 'amazon.com.tr', 'eu-west-1'],
|
48
|
-
[:ae, 'amazon.ae', 'eu-west-1'],
|
49
|
-
[:gb, 'amazon.co.uk', 'eu-west-1'],
|
50
|
-
[:us, 'amazon.com', 'us-east-1']
|
51
|
-
].map { |attributes| new(*attributes) }
|
52
61
|
end
|
53
62
|
end
|
data/lib/vacuum/matcher.rb
CHANGED
@@ -4,20 +4,35 @@ require 'json'
|
|
4
4
|
|
5
5
|
module Vacuum
|
6
6
|
# Custom VCR matcher for stubbing calls to the Product Advertising API
|
7
|
-
#
|
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
|
8
18
|
class Matcher
|
9
19
|
IGNORED_KEYS = %w[PartnerTag].freeze
|
20
|
+
private_constant :IGNORED_KEYS
|
10
21
|
|
22
|
+
# @!visibility private
|
11
23
|
attr_reader :requests
|
12
24
|
|
25
|
+
# @!visibility private
|
13
26
|
def self.call(*requests)
|
14
27
|
new(*requests).compare
|
15
28
|
end
|
16
29
|
|
30
|
+
# @!visibility private
|
17
31
|
def initialize(*requests)
|
18
32
|
@requests = requests
|
19
33
|
end
|
20
34
|
|
35
|
+
# @!visibility private
|
21
36
|
def compare
|
22
37
|
uris.reduce(:==) && bodies.reduce(:==)
|
23
38
|
end
|
@@ -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,35 +1,35 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
4
|
-
require 'httpi'
|
5
|
-
require 'json'
|
3
|
+
require 'http'
|
6
4
|
|
7
5
|
require 'vacuum/locale'
|
6
|
+
require 'vacuum/operation'
|
8
7
|
require 'vacuum/response'
|
9
8
|
|
10
9
|
module Vacuum
|
11
10
|
# A request to the Amazon Product Advertising API
|
12
11
|
class Request
|
13
|
-
|
14
|
-
|
12
|
+
# @return [HTTP::Client]
|
13
|
+
attr_reader :client
|
15
14
|
|
16
|
-
# @
|
17
|
-
attr_reader :
|
15
|
+
# @return [Locale]
|
16
|
+
attr_reader :locale
|
17
|
+
|
18
|
+
# @return [Operation]
|
19
|
+
attr_reader :operation
|
18
20
|
|
19
21
|
# Creates a new request
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
@
|
30
|
-
@
|
31
|
-
@partner_tag = partner_tag
|
32
|
-
@partner_type = partner_type
|
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
33
|
end
|
34
34
|
|
35
35
|
# Returns details about specified browse nodes
|
@@ -119,69 +119,22 @@ module Vacuum
|
|
119
119
|
request('SearchItems', params)
|
120
120
|
end
|
121
121
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
request = HTTPI::Request.new(
|
129
|
-
headers: request_headers(operation, signature),
|
130
|
-
url: locale.build_url(operation),
|
131
|
-
body: body
|
132
|
-
)
|
133
|
-
|
134
|
-
Response.new(HTTPI.post(request))
|
135
|
-
end
|
136
|
-
|
137
|
-
def sign(operation, body)
|
138
|
-
signer.sign_request(
|
139
|
-
http_method: 'POST',
|
140
|
-
url: locale.build_url(operation),
|
141
|
-
headers: headers(operation),
|
142
|
-
body: body
|
143
|
-
)
|
122
|
+
# Creates a persistent connection for multiple requests
|
123
|
+
#
|
124
|
+
# @return [self]
|
125
|
+
def persistent
|
126
|
+
@client = client.persistent("https://#{locale.host}")
|
127
|
+
self
|
144
128
|
end
|
145
129
|
|
146
|
-
|
147
|
-
headers(operation).merge(
|
148
|
-
'Content-Type' => 'application/json; charset=utf-8',
|
149
|
-
'Authorization' => signature.headers['authorization'],
|
150
|
-
'X-Amz-Content-Sha256' => signature.headers['x-amz-content-sha256'],
|
151
|
-
'X-Amz-Date' => signature.headers['x-amz-date'],
|
152
|
-
'Host' => locale.endpoint
|
153
|
-
)
|
154
|
-
end
|
155
|
-
|
156
|
-
def headers(operation)
|
157
|
-
{
|
158
|
-
'X-Amz-Target' => "com.amazon.paapi5.v1.#{SERVICE}v1.#{operation}",
|
159
|
-
'Content-Encoding' => 'amz-1.0'
|
160
|
-
}
|
161
|
-
end
|
162
|
-
|
163
|
-
def signer
|
164
|
-
Aws::Sigv4::Signer.new(
|
165
|
-
service: SERVICE,
|
166
|
-
region: locale.region,
|
167
|
-
access_key_id: access_key,
|
168
|
-
secret_access_key: secret_key,
|
169
|
-
http_method: 'POST',
|
170
|
-
endpoint: locale.endpoint
|
171
|
-
)
|
172
|
-
end
|
173
|
-
|
174
|
-
def build_body(params)
|
175
|
-
hsh = { 'PartnerTag' => partner_tag,
|
176
|
-
'PartnerType' => partner_type }
|
130
|
+
private
|
177
131
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
end
|
132
|
+
def request(operation_name, params)
|
133
|
+
@operation = Operation.new(operation_name, params: params, locale: locale)
|
134
|
+
response = client.headers(operation.headers)
|
135
|
+
.post(operation.url, body: operation.body)
|
183
136
|
|
184
|
-
|
137
|
+
Response.new(response)
|
185
138
|
end
|
186
139
|
end
|
187
140
|
end
|