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.
- data/.gitignore +19 -0
- data/.sublime-project +19 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +111 -0
- data/Rakefile +1 -0
- data/lib/mws.rb +34 -0
- data/lib/mws/apis.rb +6 -0
- data/lib/mws/apis/feeds.rb +20 -0
- data/lib/mws/apis/feeds/api.rb +103 -0
- data/lib/mws/apis/feeds/distance.rb +23 -0
- data/lib/mws/apis/feeds/feed.rb +114 -0
- data/lib/mws/apis/feeds/image_listing.rb +44 -0
- data/lib/mws/apis/feeds/inventory.rb +77 -0
- data/lib/mws/apis/feeds/measurement.rb +32 -0
- data/lib/mws/apis/feeds/money.rb +31 -0
- data/lib/mws/apis/feeds/price_listing.rb +48 -0
- data/lib/mws/apis/feeds/product.rb +173 -0
- data/lib/mws/apis/feeds/sale_price.rb +31 -0
- data/lib/mws/apis/feeds/shipping.rb +160 -0
- data/lib/mws/apis/feeds/submission_info.rb +45 -0
- data/lib/mws/apis/feeds/submission_result.rb +87 -0
- data/lib/mws/apis/feeds/transaction.rb +37 -0
- data/lib/mws/apis/feeds/weight.rb +19 -0
- data/lib/mws/apis/orders.rb +23 -0
- data/lib/mws/connection.rb +84 -0
- data/lib/mws/enum.rb +81 -0
- data/lib/mws/errors.rb +32 -0
- data/lib/mws/query.rb +45 -0
- data/lib/mws/serializer.rb +81 -0
- data/lib/mws/signer.rb +20 -0
- data/lib/mws/utils.rb +50 -0
- data/mws.gemspec +25 -0
- data/scripts/catalog-workflow +136 -0
- data/spec/mws/apis/feeds/api_spec.rb +229 -0
- data/spec/mws/apis/feeds/distance_spec.rb +43 -0
- data/spec/mws/apis/feeds/feed_spec.rb +92 -0
- data/spec/mws/apis/feeds/image_listing_spec.rb +109 -0
- data/spec/mws/apis/feeds/inventory_spec.rb +135 -0
- data/spec/mws/apis/feeds/measurement_spec.rb +84 -0
- data/spec/mws/apis/feeds/money_spec.rb +43 -0
- data/spec/mws/apis/feeds/price_listing_spec.rb +90 -0
- data/spec/mws/apis/feeds/product_spec.rb +264 -0
- data/spec/mws/apis/feeds/shipping_spec.rb +78 -0
- data/spec/mws/apis/feeds/submission_info_spec.rb +111 -0
- data/spec/mws/apis/feeds/submission_result_spec.rb +157 -0
- data/spec/mws/apis/feeds/transaction_spec.rb +64 -0
- data/spec/mws/apis/feeds/weight_spec.rb +43 -0
- data/spec/mws/apis/orders_spec.rb +9 -0
- data/spec/mws/connection_spec.rb +331 -0
- data/spec/mws/enum_spec.rb +166 -0
- data/spec/mws/query_spec.rb +104 -0
- data/spec/mws/serializer_spec.rb +187 -0
- data/spec/mws/signer_spec.rb +67 -0
- data/spec/mws/utils_spec.rb +147 -0
- data/spec/spec_helper.rb +10 -0
- 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
|