peddler 1.6.7 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
  },