mws-connect 0.0.1

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 (57) hide show
  1. data/.gitignore +19 -0
  2. data/.sublime-project +19 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +111 -0
  6. data/Rakefile +1 -0
  7. data/lib/mws.rb +34 -0
  8. data/lib/mws/apis.rb +6 -0
  9. data/lib/mws/apis/feeds.rb +20 -0
  10. data/lib/mws/apis/feeds/api.rb +103 -0
  11. data/lib/mws/apis/feeds/distance.rb +23 -0
  12. data/lib/mws/apis/feeds/feed.rb +114 -0
  13. data/lib/mws/apis/feeds/image_listing.rb +44 -0
  14. data/lib/mws/apis/feeds/inventory.rb +77 -0
  15. data/lib/mws/apis/feeds/measurement.rb +32 -0
  16. data/lib/mws/apis/feeds/money.rb +31 -0
  17. data/lib/mws/apis/feeds/price_listing.rb +48 -0
  18. data/lib/mws/apis/feeds/product.rb +173 -0
  19. data/lib/mws/apis/feeds/sale_price.rb +31 -0
  20. data/lib/mws/apis/feeds/shipping.rb +160 -0
  21. data/lib/mws/apis/feeds/submission_info.rb +45 -0
  22. data/lib/mws/apis/feeds/submission_result.rb +87 -0
  23. data/lib/mws/apis/feeds/transaction.rb +37 -0
  24. data/lib/mws/apis/feeds/weight.rb +19 -0
  25. data/lib/mws/apis/orders.rb +23 -0
  26. data/lib/mws/connection.rb +84 -0
  27. data/lib/mws/enum.rb +81 -0
  28. data/lib/mws/errors.rb +32 -0
  29. data/lib/mws/query.rb +45 -0
  30. data/lib/mws/serializer.rb +81 -0
  31. data/lib/mws/signer.rb +20 -0
  32. data/lib/mws/utils.rb +50 -0
  33. data/mws.gemspec +25 -0
  34. data/scripts/catalog-workflow +136 -0
  35. data/spec/mws/apis/feeds/api_spec.rb +229 -0
  36. data/spec/mws/apis/feeds/distance_spec.rb +43 -0
  37. data/spec/mws/apis/feeds/feed_spec.rb +92 -0
  38. data/spec/mws/apis/feeds/image_listing_spec.rb +109 -0
  39. data/spec/mws/apis/feeds/inventory_spec.rb +135 -0
  40. data/spec/mws/apis/feeds/measurement_spec.rb +84 -0
  41. data/spec/mws/apis/feeds/money_spec.rb +43 -0
  42. data/spec/mws/apis/feeds/price_listing_spec.rb +90 -0
  43. data/spec/mws/apis/feeds/product_spec.rb +264 -0
  44. data/spec/mws/apis/feeds/shipping_spec.rb +78 -0
  45. data/spec/mws/apis/feeds/submission_info_spec.rb +111 -0
  46. data/spec/mws/apis/feeds/submission_result_spec.rb +157 -0
  47. data/spec/mws/apis/feeds/transaction_spec.rb +64 -0
  48. data/spec/mws/apis/feeds/weight_spec.rb +43 -0
  49. data/spec/mws/apis/orders_spec.rb +9 -0
  50. data/spec/mws/connection_spec.rb +331 -0
  51. data/spec/mws/enum_spec.rb +166 -0
  52. data/spec/mws/query_spec.rb +104 -0
  53. data/spec/mws/serializer_spec.rb +187 -0
  54. data/spec/mws/signer_spec.rb +67 -0
  55. data/spec/mws/utils_spec.rb +147 -0
  56. data/spec/spec_helper.rb +10 -0
  57. metadata +220 -0
@@ -0,0 +1,45 @@
1
+ module Mws::Apis::Feeds
2
+
3
+ class SubmissionInfo
4
+
5
+ private :initialize
6
+
7
+ private_class_method :new
8
+
9
+ Status = Mws::Enum.for(
10
+ done: '_DONE_',
11
+ submitted: '_SUBMITTED_',
12
+ in_progress: '_IN_PROGRESS_',
13
+ cancelled: '_CANCELLED_'
14
+ )
15
+
16
+ attr_accessor :id, :submitted, :started, :completed
17
+
18
+ Mws::Enum.sym_reader self, :type, :status
19
+
20
+ def initialize(node)
21
+ @id = node.xpath('FeedSubmissionId').first.text.to_s
22
+ @type = Feed::Type.for(node.xpath('FeedType').first.text)
23
+ @status = Status.for(node.xpath('FeedProcessingStatus').first.text)
24
+ @submitted = Time.parse(node.xpath('SubmittedDate').first.text.to_s)
25
+ node.xpath('StartedProcessingDate').each do | node |
26
+ @started = Time.parse(node.text.to_s)
27
+ end
28
+ node.xpath('CompletedProcessingDate').each do | node |
29
+ @completed = Time.parse(node.text.to_s)
30
+ end
31
+ end
32
+
33
+ def self.from_xml(node)
34
+ new node
35
+ end
36
+
37
+ def ==(other)
38
+ return true if equal? other
39
+ return false unless other.class == self.class
40
+ id == other.id
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,87 @@
1
+ module Mws::Apis::Feeds
2
+
3
+ class SubmissionResult
4
+
5
+ private :initialize
6
+
7
+ private_class_method :new
8
+
9
+ Status = Mws::Enum.for(
10
+ complete: 'Complete',
11
+ processing: 'Processing',
12
+ rejected: 'Rejected'
13
+ )
14
+
15
+ attr_reader :transaction_id, :messages_processed
16
+
17
+ Mws::Enum.sym_reader self, :status
18
+
19
+ def initialize(node)
20
+ @transaction_id = node.xpath('ProcessingReport/DocumentTransactionID').first.text.to_s
21
+ @status = Status.for(node.xpath('ProcessingReport/StatusCode').first.text)
22
+ @messages_processed = node.xpath('ProcessingReport/ProcessingSummary/MessagesProcessed').first.text.to_i
23
+
24
+ @counts = {}
25
+ [ Response::Type.SUCCESS, Response::Type.ERROR, Response::Type.WARNING ].each do | type |
26
+ @counts[type.sym] = node.xpath("ProcessingReport/ProcessingSummary/#{type.val.first}").first.text.to_i
27
+ end
28
+ @responses = {}
29
+ node.xpath('ProcessingReport/Result').each do | result_node |
30
+ response = Response.from_xml(result_node)
31
+ @responses[response.id.to_sym] = response
32
+ end
33
+ end
34
+
35
+ def self.from_xml(node)
36
+ new node
37
+ end
38
+
39
+ def ==(other)
40
+ return true if equal? other
41
+ return false unless other.class == self.class
42
+ transaction_id == other.transaction_id
43
+ end
44
+
45
+ def count_for(type)
46
+ @counts[Response::Type.for(type).sym]
47
+ end
48
+
49
+ def response_for(message_id)
50
+ @responses[message_id.to_s.to_sym]
51
+ end
52
+
53
+ class Response
54
+
55
+ Type = Mws::Enum.for(
56
+ success: [ 'MessagesSuccessful' ],
57
+ error: [ 'MessagesWithError', 'Error' ],
58
+ warning: [ 'MessagesWithWarning', 'Warning' ]
59
+ )
60
+
61
+ private :initialize
62
+
63
+ private_class_method :new
64
+
65
+ attr_reader :id, :code, :description, :additional_info
66
+
67
+ Mws::Enum.sym_reader self, :type
68
+
69
+ def initialize(node)
70
+ @id = node.xpath('MessageID').first.text.to_s
71
+ @type = Type.for(node.xpath('ResultCode').first.text.to_s)
72
+ @code = node.xpath('ResultMessageCode').first.text.to_i
73
+ @description = node.xpath('ResultDescription').first.text.to_s
74
+ node.xpath('AdditionalInfo').each do | info |
75
+ @additional_info = Mws::Serializer.new.hash_for(info, 'additional_info')
76
+ end
77
+ end
78
+
79
+ def self.from_xml(node)
80
+ new node
81
+ end
82
+
83
+ end
84
+
85
+ end
86
+
87
+ end
@@ -0,0 +1,37 @@
1
+ module Mws::Apis::Feeds
2
+
3
+ class Transaction
4
+
5
+ attr_reader :id, :type, :status, :submitted, :items
6
+
7
+ def initialize(submission_info, items=[], &item_builder)
8
+ @id = submission_info.id
9
+ @type = submission_info.type
10
+ @status = submission_info.status
11
+ @submitted = submission_info.submitted
12
+ @items = items
13
+ instance_eval &item_builder unless item_builder.nil?
14
+ end
15
+
16
+ private
17
+
18
+ def item(id, sku, operation, qualifier=nil)
19
+ @items << Item.new(id, sku, operation, qualifier)
20
+ end
21
+
22
+ class Item
23
+
24
+ attr_reader :id, :sku, :operation, :qualifier
25
+
26
+ def initialize(id, sku, operation, qualifier)
27
+ @id = id
28
+ @sku = sku
29
+ @operation = operation
30
+ @qualifier = qualifier
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,19 @@
1
+ module Mws::Apis::Feeds
2
+
3
+ class Weight < Measurement
4
+
5
+ Unit = Mws::Enum.for(
6
+ grams: 'GR',
7
+ kilograms: 'KG',
8
+ ounces: 'OZ',
9
+ pounds: 'LB',
10
+ miligrams: 'MG'
11
+ )
12
+
13
+ def initialize(amount, unit=nil)
14
+ super amount, unit || :pounds
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,23 @@
1
+ class Mws::Apis::Orders
2
+
3
+ def initialize(connection, overrides={})
4
+ @connection = connection
5
+ @param_defaults = {
6
+ market: 'ATVPDKIKX0DER'
7
+ }.merge overrides
8
+ @option_defaults = {
9
+ version: '2011-01-01',
10
+ list_pattern: '%{key}.%{ext}.%<index>d'
11
+ }
12
+ end
13
+
14
+ def list(params={})
15
+ params[:markets] ||= [ params.delete(:markets) || params.delete(:market) || @param_defaults[:market] ].flatten.compact
16
+ options = @option_defaults.merge action: 'ListOrders'
17
+ doc = @connection.get "/Orders/#{options[:version]}", params, options
18
+ doc.xpath('Orders/Order').map do | node |
19
+ 'Someday this will be an Order'
20
+ end
21
+ end
22
+
23
+ end
@@ -0,0 +1,84 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'digest/md5'
4
+ require 'logging'
5
+ require 'nokogiri'
6
+
7
+ module Mws
8
+
9
+ class Connection
10
+
11
+ attr_reader :merchant, :orders, :feeds
12
+
13
+ def initialize(overrides)
14
+ @log = Logging.logger[self]
15
+ @scheme = overrides[:scheme] || 'https'
16
+ @host = overrides[:host] || 'mws.amazonservices.com'
17
+ @merchant = overrides[:merchant]
18
+ raise Mws::Errors::ValidationError, 'A merchant identifier must be specified.' if @merchant.nil?
19
+ @access = overrides[:access]
20
+ raise Mws::Errors::ValidationError, 'An access key must be specified.' if @access.nil?
21
+ @secret = overrides[:secret]
22
+ raise Mws::Errors::ValidationError, 'A secret key must be specified.' if @secret.nil?
23
+ @orders = Apis::Orders.new self
24
+ @feeds = Apis::Feeds::Api.new self
25
+ end
26
+
27
+ def get(path, params, overrides)
28
+ request(:get, path, params, nil, overrides)
29
+ end
30
+
31
+ def post(path, params, body, overrides)
32
+ request(:post, path, params, body, overrides)
33
+ end
34
+
35
+ private
36
+
37
+ def request(method, path, params, body, overrides)
38
+ query = Query.new({
39
+ action: overrides[:action],
40
+ version: overrides[:version],
41
+ merchant: @merchant,
42
+ access: @access,
43
+ list_pattern: overrides.delete(:list_pattern)
44
+ }.merge(params))
45
+ signer = Signer.new method: method, host: @host, path: path, secret: @secret
46
+ parse response_for(method, path, signer.sign(query), body), overrides
47
+ end
48
+
49
+ def response_for(method, path, query, body)
50
+ uri = URI("#{@scheme}://#{@host}#{path}?#{query}")
51
+ @log.debug "Request URI:\n#{uri}\n"
52
+ req = Net::HTTP.const_get(method.to_s.capitalize).new (uri.request_uri)
53
+ req['User-Agent'] = 'MWS Connect/0.0.1 (Language=Ruby)'
54
+ req['Accept-Encoding'] = 'text/xml'
55
+ if req.request_body_permitted? and body
56
+ @log.debug "Body:\n#{body}"
57
+ req.content_type = 'text/xml'
58
+ req['Content-MD5'] = Digest::MD5.base64digest(body).strip
59
+ @log.debug "Hash:\n#{req['Content-MD5']}\n"
60
+ req.body = body
61
+ end
62
+ res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do | http |
63
+ http.request req
64
+ end
65
+ raise Errors::ServerError.new(code: res.code, message: res.msg) if res.body.nil?
66
+ @log.debug "Response Body:\n#{res.body}\n"
67
+ res.body
68
+ end
69
+
70
+ def parse(body, overrides)
71
+ doc = Nokogiri::XML(body)
72
+ doc.remove_namespaces!
73
+ doc.xpath('/ErrorResponse/Error').each do | error |
74
+ options = {}
75
+ error.element_children.each { |node| options[node.name.downcase.to_sym] = node.text }
76
+ raise Errors::ServerError.new(options)
77
+ end
78
+ result = doc.xpath((overrides[:xpath] || '/%{action}Response/%{action}Result') % overrides ).first
79
+ result
80
+ end
81
+
82
+ end
83
+
84
+ end
data/lib/mws/enum.rb ADDED
@@ -0,0 +1,81 @@
1
+ module Mws
2
+
3
+ class Enum
4
+
5
+ private :initialize
6
+
7
+ private_class_method :new
8
+
9
+ def initialize(entries)
10
+ @reverse = {}
11
+ @entries = []
12
+ entries.each do | key, values |
13
+ entry = EnumEntry.new(key, values)
14
+ @entries << entry
15
+ @reverse[key] = entry
16
+ values = [ values ] unless values.respond_to? :each
17
+ values.each do | value |
18
+ @reverse[value] = entry
19
+ end
20
+ end
21
+ end
22
+
23
+ def syms
24
+ @entries.map { |it| it.sym }
25
+ end
26
+
27
+ def vals
28
+ @entries.map { |it| it.val }.flatten
29
+ end
30
+
31
+ def for(it)
32
+ return it if it.instance_of? EnumEntry
33
+ @reverse[it]
34
+ end
35
+
36
+ def sym(str)
37
+ entry = self.for(str)
38
+ entry && entry.sym
39
+ end
40
+
41
+ def val(sym)
42
+ entry = self.for(sym)
43
+ entry && entry.val
44
+ end
45
+
46
+ def self.for(h)
47
+ it = new(h)
48
+ eigenclass = class << it
49
+ self
50
+ end
51
+ h.each do | key, value |
52
+ eigenclass.send(:define_method, key.to_s.upcase.to_sym) do
53
+ it.for key
54
+ end
55
+ end
56
+ it
57
+ end
58
+
59
+ def self.sym_reader(target, *attributes)
60
+ attributes.each do | attribute |
61
+ target.send(:define_method, attribute) do
62
+ entry = send(:instance_variable_get, "@#{attribute}")
63
+ entry && entry.sym
64
+ end
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ class EnumEntry
71
+
72
+ attr_reader :sym, :val
73
+
74
+ def initialize(sym, val)
75
+ @sym = sym
76
+ @val = val
77
+ end
78
+
79
+ end
80
+
81
+ end
data/lib/mws/errors.rb ADDED
@@ -0,0 +1,32 @@
1
+ module Mws::Errors
2
+
3
+ class Error < RuntimeError
4
+
5
+ end
6
+
7
+ class ServerError < Error
8
+
9
+ attr_reader :type, :code, :message, :details
10
+
11
+ def initialize(options)
12
+ @type = options[:type] || 'HTTP'
13
+ @code = options[:code]
14
+ @message = options[:message]
15
+ @details = options[:details] || options[:detail]
16
+ if @details.nil? or @details.empty?
17
+ @details = 'None'
18
+ end
19
+ super "Type: #{@type}, Code: #{@code}, Message: #{@message}, Details: #{@details}"
20
+ end
21
+
22
+ end
23
+
24
+ class ClientError < Error
25
+
26
+ end
27
+
28
+ class ValidationError < ClientError
29
+
30
+ end
31
+
32
+ end
data/lib/mws/query.rb ADDED
@@ -0,0 +1,45 @@
1
+ require 'time'
2
+
3
+ class Mws::Query
4
+
5
+ def initialize(overrides)
6
+ options = {
7
+ signature_method: 'HmacSHA256',
8
+ signature_version: '2',
9
+ timestamp: Time.now.iso8601
10
+ }.merge overrides
11
+
12
+ options[:aws_access_key_id] ||= options.delete :access
13
+ options[:seller_id] ||= options.delete(:merchant) || options.delete(:seller)
14
+ options[:marketplace_id] ||= options.delete(:markets) || []
15
+ list_pattern = options.delete(:list_pattern) || '%{key}List.%{ext}.%<index>d'
16
+
17
+ @params = Hash[options.inject({}) do | params, entry |
18
+ key = normalize_key entry.first
19
+ if entry.last.respond_to? :each_with_index
20
+ entry.last.each_with_index do | value, index |
21
+ param_key = list_pattern % { key: key, ext: entry.first.to_s.split('_').last.capitalize, index: index + 1 }
22
+ params[param_key] = normalize_val value
23
+ end
24
+ else
25
+ params[key] = normalize_val entry.last
26
+ end
27
+ params
28
+ end.sort]
29
+ end
30
+
31
+ def to_s
32
+ @params.map { |it| it.join '=' }.join '&'
33
+ end
34
+
35
+ private
36
+
37
+ def normalize_key(key)
38
+ Mws::Utils.camelize(key).sub /^Aws/, 'AWS'
39
+ end
40
+
41
+ def normalize_val(value)
42
+ Mws::Utils.uri_escape(value.respond_to?(:iso8601) ? value.iso8601 : value.to_s)
43
+ end
44
+
45
+ end