peddler 1.6.7 → 2.0.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 +69 -67
- data/lib/mws/feeds/client.rb +15 -12
- data/lib/mws/finances/client.rb +12 -11
- data/lib/mws/fulfillment_inbound_shipment/client.rb +79 -107
- data/lib/mws/fulfillment_inventory/client.rb +5 -5
- data/lib/mws/fulfillment_outbound_shipment/client.rb +36 -44
- data/lib/mws/merchant_fulfillment/client.rb +11 -17
- data/lib/mws/off_amazon_payments/client.rb +38 -68
- data/lib/mws/orders/client.rb +28 -24
- data/lib/mws/products/client.rb +118 -153
- data/lib/mws/recommendations/client.rb +13 -17
- data/lib/mws/reports/client.rb +24 -23
- data/lib/mws/sellers/client.rb +5 -5
- data/lib/mws/subscriptions/client.rb +25 -36
- data/lib/peddler/client.rb +47 -124
- data/lib/peddler/errors/builder.rb +40 -14
- data/lib/peddler/errors/class_generator.rb +34 -0
- data/lib/peddler/errors/error.rb +13 -3
- data/lib/peddler/headers.rb +27 -11
- data/lib/peddler/marketplace.rb +30 -11
- data/lib/peddler/vcr_matcher.rb +11 -1
- data/lib/peddler/version.rb +1 -1
- data/lib/peddler/xml_parser.rb +4 -2
- data/lib/peddler/xml_response_parser.rb +1 -1
- data/test/helper.rb +0 -1
- data/test/integration/test_errors.rb +2 -14
- data/test/integration/test_feeds.rb +0 -3
- data/test/integration/test_multibyte_queries.rb +1 -1
- data/test/integration/test_mws_headers.rb +3 -2
- data/test/integration/test_orders.rb +2 -1
- data/test/integration/test_products.rb +9 -9
- data/test/integration/test_recommendations.rb +1 -1
- data/test/integration/test_subscriptions.rb +2 -2
- data/test/integration_helper.rb +1 -1
- data/test/mws.yml +36 -0
- data/test/mws.yml.example +8 -12
- data/test/null_client.rb +10 -8
- data/test/unit/mws/test_feeds_client.rb +1 -2
- data/test/unit/mws/test_fulfillment_outbound_shipment_client.rb +1 -1
- data/test/unit/mws/test_off_amazon_payments_client.rb +1 -1
- data/test/unit/mws/test_orders_client.rb +7 -6
- data/test/unit/mws/test_products_client.rb +13 -28
- data/test/unit/mws/test_recommendations_client.rb +1 -2
- data/test/unit/mws/test_reports_client.rb +1 -1
- data/test/unit/mws/test_subscriptions_client.rb +1 -130
- data/test/unit/peddler/errors/test_builder.rb +54 -7
- data/test/unit/peddler/errors/test_class_generator.rb +18 -0
- data/test/unit/peddler/errors/test_error.rb +7 -2
- data/test/unit/peddler/test_client.rb +136 -190
- data/test/unit/peddler/test_flat_file_parser.rb +2 -2
- data/test/unit/peddler/test_headers.rb +19 -9
- data/test/unit/peddler/test_marketplace.rb +18 -5
- data/test/unit/peddler/test_vcr_matcher.rb +3 -1
- data/test/vcr_cassettes/Feeds.yml +4816 -5224
- data/test/vcr_cassettes/Reports.yml +3278 -2604
- metadata +8 -9
- data/lib/peddler/errors.rb +0 -12
- data/lib/peddler/errors/handler.rb +0 -59
- data/test/unit/peddler/errors/test_handler.rb +0 -62
- data/test/unit/peddler/test_errors.rb +0 -26
@@ -1,33 +1,59 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
4
|
-
require '
|
3
|
+
require 'excon'
|
4
|
+
require 'forwardable'
|
5
|
+
require 'peddler/errors/class_generator'
|
6
|
+
require 'peddler/errors/parser'
|
5
7
|
|
6
8
|
module Peddler
|
7
9
|
module Errors
|
8
10
|
# @api private
|
9
11
|
class Builder
|
10
|
-
|
12
|
+
extend Forwardable
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
+
DIGIT_AS_FIRST_CHAR = /^\d/
|
15
|
+
private_constant :DIGIT_AS_FIRST_CHAR
|
16
|
+
|
17
|
+
def_delegator :error, :response
|
18
|
+
|
19
|
+
def self.call(error)
|
20
|
+
new(error).build
|
14
21
|
end
|
15
22
|
|
16
|
-
|
17
|
-
|
23
|
+
attr_reader :error
|
24
|
+
|
25
|
+
def initialize(error)
|
26
|
+
@error = error
|
18
27
|
end
|
19
28
|
|
20
|
-
def build
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
end
|
29
|
+
def build
|
30
|
+
parse_original_response
|
31
|
+
return if bad_class_name?
|
32
|
+
error_class.new(error_message, error)
|
25
33
|
end
|
26
34
|
|
27
35
|
private
|
28
36
|
|
29
|
-
def
|
30
|
-
|
37
|
+
def bad_class_name?
|
38
|
+
error_name =~ DIGIT_AS_FIRST_CHAR
|
39
|
+
end
|
40
|
+
|
41
|
+
def error_class
|
42
|
+
Errors.const_get(error_name)
|
43
|
+
rescue NameError
|
44
|
+
ClassGenerator.call(error_name)
|
45
|
+
end
|
46
|
+
|
47
|
+
def error_name
|
48
|
+
response.code
|
49
|
+
end
|
50
|
+
|
51
|
+
def error_message
|
52
|
+
response.message
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_original_response
|
56
|
+
error.instance_variable_set :@response, Parser.new(error.response)
|
31
57
|
end
|
32
58
|
end
|
33
59
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
require 'peddler/errors/error'
|
5
|
+
|
6
|
+
module Peddler
|
7
|
+
module Errors
|
8
|
+
# @api private
|
9
|
+
class ClassGenerator
|
10
|
+
include Singleton
|
11
|
+
|
12
|
+
def self.call(name)
|
13
|
+
instance.generate(name)
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@mutex = Mutex.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def generate(name)
|
21
|
+
with_mutex do
|
22
|
+
return Errors.const_get(name) if Errors.const_defined?(name)
|
23
|
+
Errors.const_set(name, Class.new(Error))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def with_mutex
|
30
|
+
@mutex.synchronize { yield }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/peddler/errors/error.rb
CHANGED
@@ -1,27 +1,37 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'forwardable'
|
4
|
+
|
3
5
|
module Peddler
|
4
6
|
# @api private
|
5
7
|
module Errors
|
6
|
-
#
|
7
|
-
#
|
8
|
+
# These error codes are common to all Amazon MWS API sections.
|
9
|
+
#
|
10
|
+
# @see https://docs.developer.amazonservices.com/en_US/dev_guide/DG_Errors.html
|
8
11
|
CODES = %w[
|
9
12
|
AccessDenied
|
13
|
+
InputStreamDisconnected
|
10
14
|
InternalError
|
11
15
|
InvalidAccessKeyId
|
12
|
-
|
16
|
+
InvalidAddress
|
17
|
+
InvalidParameterValue
|
13
18
|
QuotaExceeded
|
14
19
|
RequestThrottled
|
20
|
+
SignatureDoesNotMatch
|
15
21
|
].freeze
|
16
22
|
|
17
23
|
# @api private
|
18
24
|
class Error < StandardError
|
25
|
+
extend Forwardable
|
26
|
+
|
19
27
|
attr_reader :cause
|
20
28
|
|
21
29
|
def initialize(msg = nil, cause = nil)
|
22
30
|
@cause = cause
|
23
31
|
super msg
|
24
32
|
end
|
33
|
+
|
34
|
+
def_delegator :cause, :response
|
25
35
|
end
|
26
36
|
|
27
37
|
CODES.each do |name|
|
data/lib/peddler/headers.rb
CHANGED
@@ -3,27 +3,43 @@
|
|
3
3
|
module Peddler
|
4
4
|
# Parses MWS-specific headers
|
5
5
|
module Headers
|
6
|
-
|
6
|
+
# The max hourly request quota for the requested operation
|
7
|
+
# @return [Integer, nil]
|
8
|
+
def mws_quota_max
|
9
|
+
return unless headers['x-mws-quota-max']
|
10
|
+
headers['x-mws-quota-max'].to_i
|
11
|
+
end
|
7
12
|
|
8
|
-
|
9
|
-
|
13
|
+
# The remaining hourly request quota for the requested operation
|
14
|
+
# @return [Integer, nil]
|
15
|
+
def mws_quota_remaining
|
16
|
+
return unless headers['x-mws-quota-remaining']
|
17
|
+
headers['x-mws-quota-remaining'].to_i
|
18
|
+
end
|
10
19
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
)
|
20
|
+
# When the hourly request quota for the requested operation resets
|
21
|
+
# @return [Time, nil]
|
22
|
+
def mws_quota_resets_on
|
23
|
+
return unless headers['x-mws-quota-resetsOn']
|
24
|
+
Time.parse(headers['x-mws-quota-resetsOn'])
|
16
25
|
end
|
17
26
|
|
18
|
-
|
27
|
+
# The ID of the request
|
28
|
+
# @return [String, nil]
|
29
|
+
def mws_request_id
|
19
30
|
headers['x-mws-request-id']
|
20
31
|
end
|
21
32
|
|
22
|
-
|
33
|
+
# The timestamp of the request
|
34
|
+
# @return [Time, nil]
|
35
|
+
def mws_timestamp
|
36
|
+
return unless headers['x-mws-timestamp']
|
23
37
|
Time.parse(headers['x-mws-timestamp'])
|
24
38
|
end
|
25
39
|
|
26
|
-
|
40
|
+
# The context of the response
|
41
|
+
# @return [String, nil]
|
42
|
+
def mws_response_context
|
27
43
|
headers['x-mws-response-context']
|
28
44
|
end
|
29
45
|
end
|
data/lib/peddler/marketplace.rb
CHANGED
@@ -7,16 +7,35 @@ module Peddler
|
|
7
7
|
class << self
|
8
8
|
attr_reader :all
|
9
9
|
|
10
|
-
def find(
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
10
|
+
def find(key)
|
11
|
+
marketplace = if key.nil?
|
12
|
+
missing_key!
|
13
|
+
elsif key.size == 2
|
14
|
+
find_by_country_code(key)
|
15
|
+
else
|
16
|
+
find_by_id(key)
|
17
|
+
end
|
18
|
+
|
19
|
+
marketplace || not_found!(key)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def find_by_country_code(country_code)
|
25
|
+
country_code = 'GB' if country_code == 'UK'
|
26
|
+
all.find { |marketplace| marketplace.country_code == country_code }
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_by_id(id)
|
30
|
+
all.find { |marketplace| marketplace.id == id }
|
31
|
+
end
|
32
|
+
|
33
|
+
def missing_key!
|
34
|
+
raise ArgumentError, 'missing marketplace'
|
35
|
+
end
|
36
|
+
|
37
|
+
def not_found!(country_code)
|
38
|
+
raise ArgumentError, %("#{country_code}" is not a valid marketplace)
|
20
39
|
end
|
21
40
|
end
|
22
41
|
|
@@ -44,7 +63,7 @@ module Peddler
|
|
44
63
|
['A1RKKUPIHCS9HS', 'ES', 'mws-eu.amazonservices.com'],
|
45
64
|
['A13V1IB3VIYZZH', 'FR', 'mws-eu.amazonservices.com'],
|
46
65
|
['APJ6JRA9NG5V4', 'IT', 'mws-eu.amazonservices.com'],
|
47
|
-
['A1F83G8C2ARO7P', '
|
66
|
+
['A1F83G8C2ARO7P', 'GB', 'mws-eu.amazonservices.com'],
|
48
67
|
['A21TJRUUN4KGV', 'IN', 'mws.amazonservices.in'],
|
49
68
|
['A39IBJ37TRP1C6', 'AU', 'mws.amazonservices.com.au'],
|
50
69
|
['A1VC38T7YXB528', 'JP', 'mws.amazonservices.jp'],
|
data/lib/peddler/vcr_matcher.rb
CHANGED
@@ -2,36 +2,46 @@
|
|
2
2
|
|
3
3
|
module Peddler
|
4
4
|
# A custom matcher that can be used to record MWS interactions when
|
5
|
-
# integration
|
5
|
+
# writing integration tests
|
6
6
|
class VCRMatcher
|
7
|
+
# @api private
|
7
8
|
TRANSIENT_PARAMS = %w[
|
8
9
|
Signature Timestamp StartDate CreatedAfter QueryStartDateTime
|
9
10
|
].freeze
|
10
11
|
|
12
|
+
# @api private
|
11
13
|
SELLER_PARAMS = %w[
|
12
14
|
AWSAccessKeyId SellerId
|
13
15
|
].freeze
|
14
16
|
|
15
17
|
class << self
|
18
|
+
# @api private
|
16
19
|
def call(*requests)
|
17
20
|
new(*requests).compare
|
18
21
|
end
|
19
22
|
|
23
|
+
# @api private
|
20
24
|
def ignored_params
|
21
25
|
@ignored_params ||= TRANSIENT_PARAMS.dup
|
22
26
|
end
|
23
27
|
|
28
|
+
# Ignore seller specific attributes when recording
|
29
|
+
# @return [void]
|
24
30
|
def ignore_seller!
|
25
31
|
ignored_params.concat(SELLER_PARAMS)
|
32
|
+
ignored_params.uniq!
|
26
33
|
end
|
27
34
|
end
|
28
35
|
|
36
|
+
# @api private
|
29
37
|
attr_reader :requests
|
30
38
|
|
39
|
+
# @api private
|
31
40
|
def initialize(*requests)
|
32
41
|
@requests = requests
|
33
42
|
end
|
34
43
|
|
44
|
+
# @api private
|
35
45
|
def compare
|
36
46
|
compare_uris && compare_bodies
|
37
47
|
end
|
data/lib/peddler/version.rb
CHANGED
data/lib/peddler/xml_parser.rb
CHANGED
@@ -11,12 +11,14 @@ module Peddler
|
|
11
11
|
extend Forwardable
|
12
12
|
include Headers
|
13
13
|
|
14
|
-
def_delegator :parse, :dig
|
14
|
+
def_delegator :parse, :dig
|
15
15
|
|
16
|
-
def
|
16
|
+
def data
|
17
17
|
@data ||= find_data
|
18
18
|
end
|
19
19
|
|
20
|
+
alias parse data
|
21
|
+
|
20
22
|
def xml
|
21
23
|
MultiXml.parse(body)
|
22
24
|
end
|
data/test/helper.rb
CHANGED
@@ -2,25 +2,13 @@
|
|
2
2
|
|
3
3
|
require 'integration_helper'
|
4
4
|
require 'mws/orders'
|
5
|
-
require 'peddler/errors/handler'
|
6
5
|
|
7
6
|
class TestErrors < IntegrationTest
|
8
7
|
use 'Orders'
|
9
8
|
|
10
|
-
def setup
|
11
|
-
@previous_error_handler = MWS::Orders::Client.error_handler
|
12
|
-
MWS::Orders::Client.error_handler = Peddler::Errors::Handler
|
13
|
-
super
|
14
|
-
end
|
15
|
-
|
16
|
-
def teardown
|
17
|
-
MWS::Orders::Client.error_handler = @previous_error_handler
|
18
|
-
super
|
19
|
-
end
|
20
|
-
|
21
9
|
def test_invalid_key
|
22
10
|
clients.each do |client|
|
23
|
-
assert_raises
|
11
|
+
assert_raises Peddler::Errors::InvalidAccessKeyId do
|
24
12
|
client.aws_access_key_id = 'foo'
|
25
13
|
client.get_order('bar')
|
26
14
|
end
|
@@ -29,7 +17,7 @@ class TestErrors < IntegrationTest
|
|
29
17
|
|
30
18
|
def test_request_throttled
|
31
19
|
clients.each do |client|
|
32
|
-
assert_raises
|
20
|
+
assert_raises Peddler::Errors::RequestThrottled do
|
33
21
|
client.get_order('foo')
|
34
22
|
end
|
35
23
|
end
|
@@ -27,9 +27,6 @@ class TestFeeds < IntegrationTest
|
|
27
27
|
feed_submission_id = res.dig('FeedSubmissionInfo', 'FeedSubmissionId')
|
28
28
|
assert feed_submission_id
|
29
29
|
|
30
|
-
res = client.get_feed_submission_result(feed_submission_id)
|
31
|
-
assert res.records_count
|
32
|
-
|
33
30
|
# Clean up
|
34
31
|
client.cancel_feed_submissions(
|
35
32
|
feed_submission_id: feed_submission_id,
|
@@ -8,7 +8,7 @@ class TestMultibyteQueries < IntegrationTest
|
|
8
8
|
|
9
9
|
def test_posts_multibyte_queries_properly
|
10
10
|
ret = clients.map do |client|
|
11
|
-
res = client.list_matching_products('félix guattari machinic eros')
|
11
|
+
res = client.list_matching_products(client.marketplace.id, 'félix guattari machinic eros')
|
12
12
|
res.body.force_encoding 'UTF-8' if defined? Ox # Ox workaround
|
13
13
|
res.parse
|
14
14
|
end
|
@@ -8,8 +8,9 @@ class TestMWSHeaders < IntegrationTest
|
|
8
8
|
|
9
9
|
def test_parses_headers
|
10
10
|
clients.each do |client|
|
11
|
-
res = client.get_lowest_priced_offers_for_asin(
|
12
|
-
|
11
|
+
res = client.get_lowest_priced_offers_for_asin(client.marketplace.id,
|
12
|
+
'1780935374', 'New')
|
13
|
+
refute_nil res.mws_quota_max
|
13
14
|
end
|
14
15
|
end
|
15
16
|
end
|
@@ -6,7 +6,8 @@ require 'mws/orders'
|
|
6
6
|
class TestOrders < IntegrationTest
|
7
7
|
def test_gets_orders
|
8
8
|
clients.each do |client|
|
9
|
-
order_ids = client.list_orders(
|
9
|
+
order_ids = client.list_orders(client.marketplace.id,
|
10
|
+
created_after: Date.new(2015),
|
10
11
|
max_results_per_page: 5)
|
11
12
|
.dig('Orders', 'Order')
|
12
13
|
.map { |order| order['AmazonOrderId'] }
|
@@ -6,49 +6,49 @@ require 'mws/products'
|
|
6
6
|
class TestProducts < IntegrationTest
|
7
7
|
def test_lists_matching_products
|
8
8
|
clients.each do |client|
|
9
|
-
res = client.list_matching_products('architecture')
|
9
|
+
res = client.list_matching_products(client.marketplace.id, 'architecture')
|
10
10
|
refute_empty res.parse
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
14
|
def test_gets_matching_product
|
15
15
|
clients.each do |client|
|
16
|
-
res = client.get_matching_product('1780935374')
|
16
|
+
res = client.get_matching_product(client.marketplace.id, '1780935374')
|
17
17
|
refute_empty res.parse
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
21
|
def test_gets_matching_product_for_id
|
22
22
|
clients.each do |client|
|
23
|
-
res = client.get_matching_product_for_id('ISBN', '9781780935379')
|
23
|
+
res = client.get_matching_product_for_id(client.marketplace.id, 'ISBN', '9781780935379')
|
24
24
|
refute_empty res.parse
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
28
|
def test_gets_competitive_pricing_for_asin
|
29
29
|
clients.each do |client|
|
30
|
-
res = client.get_competitive_pricing_for_asin('1780935374')
|
30
|
+
res = client.get_competitive_pricing_for_asin(client.marketplace.id, '1780935374')
|
31
31
|
refute_empty res.parse
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
35
|
def test_gets_lowest_offer_listings_for_asin
|
36
36
|
clients.each do |client|
|
37
|
-
res = client.get_lowest_offer_listings_for_asin('1780935374')
|
37
|
+
res = client.get_lowest_offer_listings_for_asin(client.marketplace.id, '1780935374')
|
38
38
|
refute_empty res.parse
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
42
|
def test_gets_lowest_priced_offers_for_asin
|
43
43
|
clients.each do |client|
|
44
|
-
res = client.get_lowest_priced_offers_for_asin('1780935374', 'New')
|
44
|
+
res = client.get_lowest_priced_offers_for_asin(client.marketplace.id, '1780935374', 'New')
|
45
45
|
refute_empty res.parse
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
49
|
def test_gets_product_categories_for_asin
|
50
50
|
clients.each do |client|
|
51
|
-
res = client.get_product_categories_for_asin('1780935374')
|
51
|
+
res = client.get_product_categories_for_asin(client.marketplace.id, '1780935374')
|
52
52
|
refute_empty res.parse
|
53
53
|
end
|
54
54
|
end
|
@@ -56,12 +56,12 @@ class TestProducts < IntegrationTest
|
|
56
56
|
def test_gets_my_fees_estimate
|
57
57
|
clients.each do |client|
|
58
58
|
res = client.get_my_fees_estimate(
|
59
|
-
marketplace_id: client.
|
59
|
+
marketplace_id: client.marketplace.id,
|
60
60
|
id_type: 'ASIN',
|
61
61
|
id_value: '1780935374',
|
62
62
|
price_to_estimate_fees: {
|
63
63
|
listing_price: {
|
64
|
-
currency_code: currency_code_for(client.
|
64
|
+
currency_code: currency_code_for(client.marketplace.id),
|
65
65
|
amount: 100
|
66
66
|
}
|
67
67
|
},
|