peddler 1.6.7 → 2.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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -67
  3. data/lib/mws/feeds/client.rb +15 -12
  4. data/lib/mws/finances/client.rb +12 -11
  5. data/lib/mws/fulfillment_inbound_shipment/client.rb +79 -107
  6. data/lib/mws/fulfillment_inventory/client.rb +5 -5
  7. data/lib/mws/fulfillment_outbound_shipment/client.rb +36 -44
  8. data/lib/mws/merchant_fulfillment/client.rb +11 -17
  9. data/lib/mws/off_amazon_payments/client.rb +38 -68
  10. data/lib/mws/orders/client.rb +28 -24
  11. data/lib/mws/products/client.rb +118 -153
  12. data/lib/mws/recommendations/client.rb +13 -17
  13. data/lib/mws/reports/client.rb +24 -23
  14. data/lib/mws/sellers/client.rb +5 -5
  15. data/lib/mws/subscriptions/client.rb +25 -36
  16. data/lib/peddler/client.rb +47 -124
  17. data/lib/peddler/errors/builder.rb +40 -14
  18. data/lib/peddler/errors/class_generator.rb +34 -0
  19. data/lib/peddler/errors/error.rb +13 -3
  20. data/lib/peddler/headers.rb +27 -11
  21. data/lib/peddler/marketplace.rb +30 -11
  22. data/lib/peddler/vcr_matcher.rb +11 -1
  23. data/lib/peddler/version.rb +1 -1
  24. data/lib/peddler/xml_parser.rb +4 -2
  25. data/lib/peddler/xml_response_parser.rb +1 -1
  26. data/test/helper.rb +0 -1
  27. data/test/integration/test_errors.rb +2 -14
  28. data/test/integration/test_feeds.rb +0 -3
  29. data/test/integration/test_multibyte_queries.rb +1 -1
  30. data/test/integration/test_mws_headers.rb +3 -2
  31. data/test/integration/test_orders.rb +2 -1
  32. data/test/integration/test_products.rb +9 -9
  33. data/test/integration/test_recommendations.rb +1 -1
  34. data/test/integration/test_subscriptions.rb +2 -2
  35. data/test/integration_helper.rb +1 -1
  36. data/test/mws.yml +36 -0
  37. data/test/mws.yml.example +8 -12
  38. data/test/null_client.rb +10 -8
  39. data/test/unit/mws/test_feeds_client.rb +1 -2
  40. data/test/unit/mws/test_fulfillment_outbound_shipment_client.rb +1 -1
  41. data/test/unit/mws/test_off_amazon_payments_client.rb +1 -1
  42. data/test/unit/mws/test_orders_client.rb +7 -6
  43. data/test/unit/mws/test_products_client.rb +13 -28
  44. data/test/unit/mws/test_recommendations_client.rb +1 -2
  45. data/test/unit/mws/test_reports_client.rb +1 -1
  46. data/test/unit/mws/test_subscriptions_client.rb +1 -130
  47. data/test/unit/peddler/errors/test_builder.rb +54 -7
  48. data/test/unit/peddler/errors/test_class_generator.rb +18 -0
  49. data/test/unit/peddler/errors/test_error.rb +7 -2
  50. data/test/unit/peddler/test_client.rb +136 -190
  51. data/test/unit/peddler/test_flat_file_parser.rb +2 -2
  52. data/test/unit/peddler/test_headers.rb +19 -9
  53. data/test/unit/peddler/test_marketplace.rb +18 -5
  54. data/test/unit/peddler/test_vcr_matcher.rb +3 -1
  55. data/test/vcr_cassettes/Feeds.yml +4816 -5224
  56. data/test/vcr_cassettes/Reports.yml +3278 -2604
  57. metadata +8 -9
  58. data/lib/peddler/errors.rb +0 -12
  59. data/lib/peddler/errors/handler.rb +0 -59
  60. data/test/unit/peddler/errors/test_handler.rb +0 -62
  61. data/test/unit/peddler/test_errors.rb +0 -26
@@ -1,33 +1,59 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'singleton'
4
- require 'peddler/errors/error'
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
- include Singleton
12
+ extend Forwardable
11
13
 
12
- def self.build(name)
13
- instance.build(name)
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
- def initialize
17
- @mutex = Mutex.new
23
+ attr_reader :error
24
+
25
+ def initialize(error)
26
+ @error = error
18
27
  end
19
28
 
20
- def build(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
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 with_mutex
30
- @mutex.synchronize { yield }
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
@@ -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
- # Here I curate error classes I see value in creating up front so we can use
7
- # them for control flow. All other errors will be created at runtime.
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
- InvalidMarketplace
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|
@@ -3,27 +3,43 @@
3
3
  module Peddler
4
4
  # Parses MWS-specific headers
5
5
  module Headers
6
- Quota = Struct.new(:max, :remaining, :resets_on)
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
- def quota
9
- return if headers.keys.none? { |key| key.include?('quota') }
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
- Quota.new(
12
- headers['x-mws-quota-max'].to_i,
13
- headers['x-mws-quota-remaining'].to_i,
14
- Time.parse(headers['x-mws-quota-resetsOn'])
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
- def request_id
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
- def timestamp
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
- def response_context
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
@@ -7,16 +7,35 @@ module Peddler
7
7
  class << self
8
8
  attr_reader :all
9
9
 
10
- def find(id)
11
- all.find { |marketplace| marketplace.id == id } || begin
12
- message = if id
13
- %("#{id}" is not a valid MarketplaceId)
14
- else
15
- 'missing MarketplaceId'
16
- end
17
-
18
- raise ArgumentError, message
19
- end
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', 'UK', 'mws-eu.amazonservices.com'],
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'],
@@ -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-testing
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Peddler
4
- VERSION = '1.6.7'
4
+ VERSION = '2.0.0'
5
5
  end
@@ -11,12 +11,14 @@ module Peddler
11
11
  extend Forwardable
12
12
  include Headers
13
13
 
14
- def_delegator :parse, :dig if Hash.method_defined?(:dig)
14
+ def_delegator :parse, :dig
15
15
 
16
- def parse
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
@@ -18,7 +18,7 @@ module Peddler
18
18
  payload = xml.values.first
19
19
  found = payload.find { |k, _| k.match(MATCHER) }
20
20
 
21
- found.last if found
21
+ found&.last
22
22
  end
23
23
  end
24
24
  end
@@ -6,7 +6,6 @@ SimpleCov.start do
6
6
  add_filter '/test/'
7
7
  end
8
8
 
9
- require 'backports/2.3.0/hash/dig'
10
9
  require 'minitest/autorun'
11
10
  require 'minitest/focus'
12
11
  begin
@@ -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(Peddler::Errors::InvalidAccessKeyId) do
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(Peddler::Errors::RequestThrottled) do
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('1780935374', 'New')
12
- refute_nil res.quota
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(created_after: Date.new(2015),
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.primary_marketplace_id,
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.primary_marketplace_id),
64
+ currency_code: currency_code_for(client.marketplace.id),
65
65
  amount: 100
66
66
  }
67
67
  },