1# Copyright (c) 2012-2014 Andy Davidoff http://www.disruptek.com/
2#
3# Permission is hereby granted, free of charge, to any person obtaining a
4# copy of this software and associated documentation files (the
5# "Software"), to deal in the Software without restriction, including
6# without limitation the rights to use, copy, modify, merge, publish, dis-
7# tribute, sublicense, and/or sell copies of the Software, and to permit
8# persons to whom the Software is furnished to do so, subject to the fol-
9# lowing conditions:
10#
11# The above copyright notice and this permission notice shall be included
12# in all copies or substantial portions of the Software.
13#
14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
16# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
17# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
18# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20# IN THE SOFTWARE.
21import xml.sax
22import hashlib
23import string
24import collections
25from boto.connection import AWSQueryConnection
26from boto.exception import BotoServerError
27import boto.mws.exception
28import boto.mws.response
29from boto.handler import XmlHandler
30from boto.compat import filter, map, six, encodebytes
31
32__all__ = ['MWSConnection']
33
34api_version_path = {
35    'Feeds':             ('2009-01-01', 'Merchant', '/'),
36    'Reports':           ('2009-01-01', 'Merchant', '/'),
37    'Orders':            ('2013-09-01', 'SellerId', '/Orders/2013-09-01'),
38    'Products':          ('2011-10-01', 'SellerId', '/Products/2011-10-01'),
39    'Sellers':           ('2011-07-01', 'SellerId', '/Sellers/2011-07-01'),
40    'Inbound':           ('2010-10-01', 'SellerId',
41                          '/FulfillmentInboundShipment/2010-10-01'),
42    'Outbound':          ('2010-10-01', 'SellerId',
43                          '/FulfillmentOutboundShipment/2010-10-01'),
44    'Inventory':         ('2010-10-01', 'SellerId',
45                          '/FulfillmentInventory/2010-10-01'),
46    'Recommendations':   ('2013-04-01', 'SellerId',
47                          '/Recommendations/2013-04-01'),
48    'CustomerInfo':      ('2014-03-01', 'SellerId',
49                          '/CustomerInformation/2014-03-01'),
50    'CartInfo':          ('2014-03-01', 'SellerId',
51                          '/CartInformation/2014-03-01'),
52    'Subscriptions':     ('2013-07-01', 'SellerId',
53                          '/Subscriptions/2013-07-01'),
54    'OffAmazonPayments': ('2013-01-01', 'SellerId',
55                          '/OffAmazonPayments/2013-01-01'),
56}
57content_md5 = lambda c: encodebytes(hashlib.md5(c).digest()).strip()
58decorated_attrs = ('action', 'response', 'section',
59                   'quota', 'restore', 'version')
60api_call_map = {}
61
62
63def add_attrs_from(func, to):
64    for attr in decorated_attrs:
65        setattr(to, attr, getattr(func, attr, None))
66    to.__wrapped__ = func
67    return to
68
69
70def structured_lists(*fields):
71
72    def decorator(func):
73
74        def wrapper(self, *args, **kw):
75            for key, acc in [f.split('.') for f in fields]:
76                if key in kw:
77                    newkey = key + '.' + acc + (acc and '.' or '')
78                    for i in range(len(kw[key])):
79                        kw[newkey + str(i + 1)] = kw[key][i]
80                    kw.pop(key)
81            return func(self, *args, **kw)
82        wrapper.__doc__ = "{0}\nLists: {1}".format(func.__doc__,
83                                                   ', '.join(fields))
84        return add_attrs_from(func, to=wrapper)
85    return decorator
86
87
88def http_body(field):
89
90    def decorator(func):
91
92        def wrapper(*args, **kw):
93            if any([f not in kw for f in (field, 'content_type')]):
94                message = "{0} requires {1} and content_type arguments for " \
95                          "building HTTP body".format(func.action, field)
96                raise KeyError(message)
97            kw['body'] = kw.pop(field)
98            kw['headers'] = {
99                'Content-Type': kw.pop('content_type'),
100                'Content-MD5':  content_md5(kw['body']),
101            }
102            return func(*args, **kw)
103        wrapper.__doc__ = "{0}\nRequired HTTP Body: " \
104                          "{1}".format(func.__doc__, field)
105        return add_attrs_from(func, to=wrapper)
106    return decorator
107
108
109def destructure_object(value, into, prefix, members=False):
110    if isinstance(value, boto.mws.response.ResponseElement):
111        destructure_object(value.__dict__, into, prefix, members=members)
112    elif isinstance(value, collections.Mapping):
113        for name in value:
114            if name.startswith('_'):
115                continue
116            destructure_object(value[name], into, prefix + '.' + name,
117                               members=members)
118    elif isinstance(value, six.string_types):
119        into[prefix] = value
120    elif isinstance(value, collections.Iterable):
121        for index, element in enumerate(value):
122            suffix = (members and '.member.' or '.') + str(index + 1)
123            destructure_object(element, into, prefix + suffix,
124                               members=members)
125    elif isinstance(value, bool):
126        into[prefix] = str(value).lower()
127    else:
128        into[prefix] = value
129
130
131def structured_objects(*fields, **kwargs):
132
133    def decorator(func):
134
135        def wrapper(*args, **kw):
136            members = kwargs.get('members', False)
137            for field in filter(lambda i: i in kw, fields):
138                destructure_object(kw.pop(field), kw, field, members=members)
139            return func(*args, **kw)
140        wrapper.__doc__ = "{0}\nElement|Iter|Map: {1}\n" \
141                          "(ResponseElement or anything iterable/dict-like)" \
142                          .format(func.__doc__, ', '.join(fields))
143        return add_attrs_from(func, to=wrapper)
144    return decorator
145
146
147def requires(*groups):
148
149    def decorator(func):
150
151        def requires(*args, **kw):
152            hasgroup = lambda group: all(key in kw for key in group)
153            if 1 != len(list(filter(hasgroup, groups))):
154                message = ' OR '.join(['+'.join(g) for g in groups])
155                message = "{0} requires {1} argument(s)" \
156                          "".format(func.action, message)
157                raise KeyError(message)
158            return func(*args, **kw)
159        message = ' OR '.join(['+'.join(g) for g in groups])
160        requires.__doc__ = "{0}\nRequired: {1}".format(func.__doc__,
161                                                       message)
162        return add_attrs_from(func, to=requires)
163    return decorator
164
165
166def exclusive(*groups):
167
168    def decorator(func):
169
170        def wrapper(*args, **kw):
171            hasgroup = lambda group: all(key in kw for key in group)
172            if len(list(filter(hasgroup, groups))) not in (0, 1):
173                message = ' OR '.join(['+'.join(g) for g in groups])
174                message = "{0} requires either {1}" \
175                          "".format(func.action, message)
176                raise KeyError(message)
177            return func(*args, **kw)
178        message = ' OR '.join(['+'.join(g) for g in groups])
179        wrapper.__doc__ = "{0}\nEither: {1}".format(func.__doc__,
180                                                    message)
181        return add_attrs_from(func, to=wrapper)
182    return decorator
183
184
185def dependent(field, *groups):
186
187    def decorator(func):
188
189        def wrapper(*args, **kw):
190            hasgroup = lambda group: all(key in kw for key in group)
191            if field in kw and not any(hasgroup(g) for g in groups):
192                message = ' OR '.join(['+'.join(g) for g in groups])
193                message = "{0} argument {1} requires {2}" \
194                          "".format(func.action, field, message)
195                raise KeyError(message)
196            return func(*args, **kw)
197        message = ' OR '.join(['+'.join(g) for g in groups])
198        wrapper.__doc__ = "{0}\n{1} requires: {2}".format(func.__doc__,
199                                                          field,
200                                                          message)
201        return add_attrs_from(func, to=wrapper)
202    return decorator
203
204
205def requires_some_of(*fields):
206
207    def decorator(func):
208
209        def requires(*args, **kw):
210            if not any(i in kw for i in fields):
211                message = "{0} requires at least one of {1} argument(s)" \
212                          "".format(func.action, ', '.join(fields))
213                raise KeyError(message)
214            return func(*args, **kw)
215        requires.__doc__ = "{0}\nSome Required: {1}".format(func.__doc__,
216                                                            ', '.join(fields))
217        return add_attrs_from(func, to=requires)
218    return decorator
219
220
221def boolean_arguments(*fields):
222
223    def decorator(func):
224
225        def wrapper(*args, **kw):
226            for field in [f for f in fields if isinstance(kw.get(f), bool)]:
227                kw[field] = str(kw[field]).lower()
228            return func(*args, **kw)
229        wrapper.__doc__ = "{0}\nBooleans: {1}".format(func.__doc__,
230                                                      ', '.join(fields))
231        return add_attrs_from(func, to=wrapper)
232    return decorator
233
234
235def api_action(section, quota, restore, *api):
236
237    def decorator(func, quota=int(quota), restore=float(restore)):
238        version, accesskey, path = api_version_path[section]
239        action = ''.join(api or map(str.capitalize, func.__name__.split('_')))
240
241        def wrapper(self, *args, **kw):
242            kw.setdefault(accesskey, getattr(self, accesskey, None))
243            if kw[accesskey] is None:
244                message = "{0} requires {1} argument. Set the " \
245                          "MWSConnection.{2} attribute?" \
246                          "".format(action, accesskey, accesskey)
247                raise KeyError(message)
248            kw['Action'] = action
249            kw['Version'] = version
250            response = self._response_factory(action, connection=self)
251            request = dict(path=path, quota=quota, restore=restore)
252            return func(self, request, response, *args, **kw)
253        for attr in decorated_attrs:
254            setattr(wrapper, attr, locals().get(attr))
255        wrapper.__doc__ = "MWS {0}/{1} API call; quota={2} restore={3:.2f}\n" \
256                          "{4}".format(action, version, quota, restore,
257                                       func.__doc__)
258        api_call_map[action] = func.__name__
259        return wrapper
260    return decorator
261
262
263class MWSConnection(AWSQueryConnection):
264
265    ResponseFactory = boto.mws.response.ResponseFactory
266    ResponseErrorFactory = boto.mws.exception.ResponseErrorFactory
267
268    def __init__(self, *args, **kw):
269        kw.setdefault('host', 'mws.amazonservices.com')
270        self._sandboxed = kw.pop('sandbox', False)
271        self.Merchant = kw.pop('Merchant', None) or kw.get('SellerId')
272        self.SellerId = kw.pop('SellerId', None) or self.Merchant
273        kw = self._setup_factories(kw.pop('factory_scopes', []), **kw)
274        super(MWSConnection, self).__init__(*args, **kw)
275
276    def _setup_factories(self, extrascopes, **kw):
277        for factory, (scope, Default) in {
278            'response_factory':
279                (boto.mws.response, self.ResponseFactory),
280            'response_error_factory':
281                (boto.mws.exception, self.ResponseErrorFactory),
282        }.items():
283            if factory in kw:
284                setattr(self, '_' + factory, kw.pop(factory))
285            else:
286                scopes = extrascopes + [scope]
287                setattr(self, '_' + factory, Default(scopes=scopes))
288        return kw
289
290    def _sandboxify(self, path):
291        if not self._sandboxed:
292            return path
293        splat = path.split('/')
294        splat[-2] += '_Sandbox'
295        return '/'.join(splat)
296
297    def _required_auth_capability(self):
298        return ['mws']
299
300    def _post_request(self, request, params, parser, body='', headers=None):
301        """Make a POST request, optionally with a content body,
302           and return the response, optionally as raw text.
303        """
304        headers = headers or {}
305        path = self._sandboxify(request['path'])
306        request = self.build_base_http_request('POST', path, None, data=body,
307                                               params=params, headers=headers,
308                                               host=self.host)
309        try:
310            response = self._mexe(request, override_num_retries=None)
311        except BotoServerError as bs:
312            raise self._response_error_factory(bs.status, bs.reason, bs.body)
313        body = response.read()
314        boto.log.debug(body)
315        if not body:
316            boto.log.error('Null body %s' % body)
317            raise self._response_error_factory(response.status,
318                                               response.reason, body)
319        if response.status != 200:
320            boto.log.error('%s %s' % (response.status, response.reason))
321            boto.log.error('%s' % body)
322            raise self._response_error_factory(response.status,
323                                               response.reason, body)
324        digest = response.getheader('Content-MD5')
325        if digest is not None:
326            assert content_md5(body) == digest
327        contenttype = response.getheader('Content-Type')
328        return self._parse_response(parser, contenttype, body)
329
330    def _parse_response(self, parser, contenttype, body):
331        if not contenttype.startswith('text/xml'):
332            return body
333        handler = XmlHandler(parser, self)
334        xml.sax.parseString(body, handler)
335        return parser
336
337    def method_for(self, name):
338        """Return the MWS API method referred to in the argument.
339           The named method can be in CamelCase or underlined_lower_case.
340           This is the complement to MWSConnection.any_call.action
341        """
342        action = '_' in name and string.capwords(name, '_') or name
343        if action in api_call_map:
344            return getattr(self, api_call_map[action])
345        return None
346
347    def iter_call(self, call, *args, **kw):
348        """Pass a call name as the first argument and a generator
349           is returned for the initial response and any continuation
350           call responses made using the NextToken.
351        """
352        method = self.method_for(call)
353        assert method, 'No call named "{0}"'.format(call)
354        return self.iter_response(method(*args, **kw))
355
356    def iter_response(self, response):
357        """Pass a call's response as the initial argument and a
358           generator is returned for the initial response and any
359           continuation call responses made using the NextToken.
360        """
361        yield response
362        more = self.method_for(response._action + 'ByNextToken')
363        while more and response._result.HasNext == 'true':
364            response = more(NextToken=response._result.NextToken)
365            yield response
366
367    @requires(['FeedType'])
368    @boolean_arguments('PurgeAndReplace')
369    @http_body('FeedContent')
370    @structured_lists('MarketplaceIdList.Id')
371    @api_action('Feeds', 15, 120)
372    def submit_feed(self, request, response, headers=None, body='', **kw):
373        """Uploads a feed for processing by Amazon MWS.
374        """
375        headers = headers or {}
376        return self._post_request(request, kw, response, body=body,
377                                  headers=headers)
378
379    @structured_lists('FeedSubmissionIdList.Id', 'FeedTypeList.Type',
380                      'FeedProcessingStatusList.Status')
381    @api_action('Feeds', 10, 45)
382    def get_feed_submission_list(self, request, response, **kw):
383        """Returns a list of all feed submissions submitted in the
384           previous 90 days.
385        """
386        return self._post_request(request, kw, response)
387
388    @requires(['NextToken'])
389    @api_action('Feeds', 0, 0)
390    def get_feed_submission_list_by_next_token(self, request, response, **kw):
391        """Returns a list of feed submissions using the NextToken parameter.
392        """
393        return self._post_request(request, kw, response)
394
395    @structured_lists('FeedTypeList.Type', 'FeedProcessingStatusList.Status')
396    @api_action('Feeds', 10, 45)
397    def get_feed_submission_count(self, request, response, **kw):
398        """Returns a count of the feeds submitted in the previous 90 days.
399        """
400        return self._post_request(request, kw, response)
401
402    @structured_lists('FeedSubmissionIdList.Id', 'FeedTypeList.Type')
403    @api_action('Feeds', 10, 45)
404    def cancel_feed_submissions(self, request, response, **kw):
405        """Cancels one or more feed submissions and returns a
406           count of the feed submissions that were canceled.
407        """
408        return self._post_request(request, kw, response)
409
410    @requires(['FeedSubmissionId'])
411    @api_action('Feeds', 15, 60)
412    def get_feed_submission_result(self, request, response, **kw):
413        """Returns the feed processing report.
414        """
415        return self._post_request(request, kw, response)
416
417    def get_service_status(self, **kw):
418        """Instruct the user on how to get service status.
419        """
420        sections = ', '.join(map(str.lower, api_version_path.keys()))
421        message = "Use {0}.get_(section)_service_status(), " \
422                  "where (section) is one of the following: " \
423                  "{1}".format(self.__class__.__name__, sections)
424        raise AttributeError(message)
425
426    @requires(['ReportType'])
427    @structured_lists('MarketplaceIdList.Id')
428    @boolean_arguments('ReportOptions=ShowSalesChannel')
429    @api_action('Reports', 15, 60)
430    def request_report(self, request, response, **kw):
431        """Creates a report request and submits the request to Amazon MWS.
432        """
433        return self._post_request(request, kw, response)
434
435    @structured_lists('ReportRequestIdList.Id', 'ReportTypeList.Type',
436                      'ReportProcessingStatusList.Status')
437    @api_action('Reports', 10, 45)
438    def get_report_request_list(self, request, response, **kw):
439        """Returns a list of report requests that you can use to get the
440           ReportRequestId for a report.
441        """
442        return self._post_request(request, kw, response)
443
444    @requires(['NextToken'])
445    @api_action('Reports', 0, 0)
446    def get_report_request_list_by_next_token(self, request, response, **kw):
447        """Returns a list of report requests using the NextToken,
448           which was supplied by a previous request to either
449           GetReportRequestListByNextToken or GetReportRequestList, where
450           the value of HasNext was true in that previous request.
451        """
452        return self._post_request(request, kw, response)
453
454    @structured_lists('ReportTypeList.Type',
455                      'ReportProcessingStatusList.Status')
456    @api_action('Reports', 10, 45)
457    def get_report_request_count(self, request, response, **kw):
458        """Returns a count of report requests that have been submitted
459           to Amazon MWS for processing.
460        """
461        return self._post_request(request, kw, response)
462
463    @api_action('Reports', 10, 45)
464    def cancel_report_requests(self, request, response, **kw):
465        """Cancel one or more report requests, returning the count of the
466           canceled report requests and the report request information.
467        """
468        return self._post_request(request, kw, response)
469
470    @boolean_arguments('Acknowledged')
471    @structured_lists('ReportRequestIdList.Id', 'ReportTypeList.Type')
472    @api_action('Reports', 10, 60)
473    def get_report_list(self, request, response, **kw):
474        """Returns a list of reports that were created in the previous
475           90 days that match the query parameters.
476        """
477        return self._post_request(request, kw, response)
478
479    @requires(['NextToken'])
480    @api_action('Reports', 0, 0)
481    def get_report_list_by_next_token(self, request, response, **kw):
482        """Returns a list of reports using the NextToken, which
483           was supplied by a previous request to either
484           GetReportListByNextToken or GetReportList, where the
485           value of HasNext was true in the previous call.
486        """
487        return self._post_request(request, kw, response)
488
489    @boolean_arguments('Acknowledged')
490    @structured_lists('ReportTypeList.Type')
491    @api_action('Reports', 10, 45)
492    def get_report_count(self, request, response, **kw):
493        """Returns a count of the reports, created in the previous 90 days,
494           with a status of _DONE_ and that are available for download.
495        """
496        return self._post_request(request, kw, response)
497
498    @requires(['ReportId'])
499    @api_action('Reports', 15, 60)
500    def get_report(self, request, response, **kw):
501        """Returns the contents of a report.
502        """
503        return self._post_request(request, kw, response)
504
505    @requires(['ReportType', 'Schedule'])
506    @api_action('Reports', 10, 45)
507    def manage_report_schedule(self, request, response, **kw):
508        """Creates, updates, or deletes a report request schedule for
509           a specified report type.
510        """
511        return self._post_request(request, kw, response)
512
513    @structured_lists('ReportTypeList.Type')
514    @api_action('Reports', 10, 45)
515    def get_report_schedule_list(self, request, response, **kw):
516        """Returns a list of order report requests that are scheduled
517           to be submitted to Amazon MWS for processing.
518        """
519        return self._post_request(request, kw, response)
520
521    @requires(['NextToken'])
522    @api_action('Reports', 0, 0)
523    def get_report_schedule_list_by_next_token(self, request, response, **kw):
524        """Returns a list of report requests using the NextToken,
525           which was supplied by a previous request to either
526           GetReportScheduleListByNextToken or GetReportScheduleList,
527           where the value of HasNext was true in that previous request.
528        """
529        return self._post_request(request, kw, response)
530
531    @structured_lists('ReportTypeList.Type')
532    @api_action('Reports', 10, 45)
533    def get_report_schedule_count(self, request, response, **kw):
534        """Returns a count of order report requests that are scheduled
535           to be submitted to Amazon MWS.
536        """
537        return self._post_request(request, kw, response)
538
539    @requires(['ReportIdList'])
540    @boolean_arguments('Acknowledged')
541    @structured_lists('ReportIdList.Id')
542    @api_action('Reports', 10, 45)
543    def update_report_acknowledgements(self, request, response, **kw):
544        """Updates the acknowledged status of one or more reports.
545        """
546        return self._post_request(request, kw, response)
547
548    @requires(['ShipFromAddress', 'InboundShipmentPlanRequestItems'])
549    @structured_objects('ShipFromAddress', 'InboundShipmentPlanRequestItems')
550    @api_action('Inbound', 30, 0.5)
551    def create_inbound_shipment_plan(self, request, response, **kw):
552        """Returns the information required to create an inbound shipment.
553        """
554        return self._post_request(request, kw, response)
555
556    @requires(['ShipmentId', 'InboundShipmentHeader', 'InboundShipmentItems'])
557    @structured_objects('InboundShipmentHeader', 'InboundShipmentItems')
558    @api_action('Inbound', 30, 0.5)
559    def create_inbound_shipment(self, request, response, **kw):
560        """Creates an inbound shipment.
561        """
562        return self._post_request(request, kw, response)
563
564    @requires(['ShipmentId'])
565    @structured_objects('InboundShipmentHeader', 'InboundShipmentItems')
566    @api_action('Inbound', 30, 0.5)
567    def update_inbound_shipment(self, request, response, **kw):
568        """Updates an existing inbound shipment.  Amazon documentation
569           is ambiguous as to whether the InboundShipmentHeader and
570           InboundShipmentItems arguments are required.
571        """
572        return self._post_request(request, kw, response)
573
574    @requires_some_of('ShipmentIdList', 'ShipmentStatusList')
575    @structured_lists('ShipmentIdList.Id', 'ShipmentStatusList.Status')
576    @api_action('Inbound', 30, 0.5)
577    def list_inbound_shipments(self, request, response, **kw):
578        """Returns a list of inbound shipments based on criteria that
579           you specify.
580        """
581        return self._post_request(request, kw, response)
582
583    @requires(['NextToken'])
584    @api_action('Inbound', 30, 0.5)
585    def list_inbound_shipments_by_next_token(self, request, response, **kw):
586        """Returns the next page of inbound shipments using the NextToken
587           parameter.
588        """
589        return self._post_request(request, kw, response)
590
591    @requires(['ShipmentId'], ['LastUpdatedAfter', 'LastUpdatedBefore'])
592    @api_action('Inbound', 30, 0.5)
593    def list_inbound_shipment_items(self, request, response, **kw):
594        """Returns a list of items in a specified inbound shipment, or a
595           list of items that were updated within a specified time frame.
596        """
597        return self._post_request(request, kw, response)
598
599    @requires(['NextToken'])
600    @api_action('Inbound', 30, 0.5)
601    def list_inbound_shipment_items_by_next_token(self, request, response, **kw):
602        """Returns the next page of inbound shipment items using the
603           NextToken parameter.
604        """
605        return self._post_request(request, kw, response)
606
607    @api_action('Inbound', 2, 300, 'GetServiceStatus')
608    def get_inbound_service_status(self, request, response, **kw):
609        """Returns the operational status of the Fulfillment Inbound
610           Shipment API section.
611        """
612        return self._post_request(request, kw, response)
613
614    @requires(['SellerSkus'], ['QueryStartDateTime'])
615    @structured_lists('SellerSkus.member')
616    @api_action('Inventory', 30, 0.5)
617    def list_inventory_supply(self, request, response, **kw):
618        """Returns information about the availability of a seller's
619           inventory.
620        """
621        return self._post_request(request, kw, response)
622
623    @requires(['NextToken'])
624    @api_action('Inventory', 30, 0.5)
625    def list_inventory_supply_by_next_token(self, request, response, **kw):
626        """Returns the next page of information about the availability
627           of a seller's inventory using the NextToken parameter.
628        """
629        return self._post_request(request, kw, response)
630
631    @api_action('Inventory', 2, 300, 'GetServiceStatus')
632    def get_inventory_service_status(self, request, response, **kw):
633        """Returns the operational status of the Fulfillment Inventory
634           API section.
635        """
636        return self._post_request(request, kw, response)
637
638    @requires(['PackageNumber'])
639    @api_action('Outbound', 30, 0.5)
640    def get_package_tracking_details(self, request, response, **kw):
641        """Returns delivery tracking information for a package in
642           an outbound shipment for a Multi-Channel Fulfillment order.
643        """
644        return self._post_request(request, kw, response)
645
646    @requires(['Address', 'Items'])
647    @structured_objects('Address', 'Items')
648    @api_action('Outbound', 30, 0.5)
649    def get_fulfillment_preview(self, request, response, **kw):
650        """Returns a list of fulfillment order previews based on items
651           and shipping speed categories that you specify.
652        """
653        return self._post_request(request, kw, response)
654
655    @requires(['SellerFulfillmentOrderId', 'DisplayableOrderId',
656               'ShippingSpeedCategory',    'DisplayableOrderDateTime',
657               'DestinationAddress',       'DisplayableOrderComment',
658               'Items'])
659    @structured_objects('DestinationAddress', 'Items')
660    @api_action('Outbound', 30, 0.5)
661    def create_fulfillment_order(self, request, response, **kw):
662        """Requests that Amazon ship items from the seller's inventory
663           to a destination address.
664        """
665        return self._post_request(request, kw, response)
666
667    @requires(['SellerFulfillmentOrderId'])
668    @api_action('Outbound', 30, 0.5)
669    def get_fulfillment_order(self, request, response, **kw):
670        """Returns a fulfillment order based on a specified
671           SellerFulfillmentOrderId.
672        """
673        return self._post_request(request, kw, response)
674
675    @api_action('Outbound', 30, 0.5)
676    def list_all_fulfillment_orders(self, request, response, **kw):
677        """Returns a list of fulfillment orders fulfilled after (or
678           at) a specified date or by fulfillment method.
679        """
680        return self._post_request(request, kw, response)
681
682    @requires(['NextToken'])
683    @api_action('Outbound', 30, 0.5)
684    def list_all_fulfillment_orders_by_next_token(self, request, response, **kw):
685        """Returns the next page of inbound shipment items using the
686           NextToken parameter.
687        """
688        return self._post_request(request, kw, response)
689
690    @requires(['SellerFulfillmentOrderId'])
691    @api_action('Outbound', 30, 0.5)
692    def cancel_fulfillment_order(self, request, response, **kw):
693        """Requests that Amazon stop attempting to fulfill an existing
694           fulfillment order.
695        """
696        return self._post_request(request, kw, response)
697
698    @api_action('Outbound', 2, 300, 'GetServiceStatus')
699    def get_outbound_service_status(self, request, response, **kw):
700        """Returns the operational status of the Fulfillment Outbound
701           API section.
702        """
703        return self._post_request(request, kw, response)
704
705    @requires(['CreatedAfter'], ['LastUpdatedAfter'])
706    @requires(['MarketplaceId'])
707    @exclusive(['CreatedAfter'], ['LastUpdatedAfter'])
708    @dependent('CreatedBefore', ['CreatedAfter'])
709    @exclusive(['LastUpdatedAfter'], ['BuyerEmail'], ['SellerOrderId'])
710    @dependent('LastUpdatedBefore', ['LastUpdatedAfter'])
711    @exclusive(['CreatedAfter'], ['LastUpdatedBefore'])
712    @structured_objects('OrderTotal', 'ShippingAddress',
713                        'PaymentExecutionDetail')
714    @structured_lists('MarketplaceId.Id', 'OrderStatus.Status',
715                      'FulfillmentChannel.Channel', 'PaymentMethod.')
716    @api_action('Orders', 6, 60)
717    def list_orders(self, request, response, **kw):
718        """Returns a list of orders created or updated during a time
719           frame that you specify.
720        """
721        toggle = set(('FulfillmentChannel.Channel.1',
722                      'OrderStatus.Status.1', 'PaymentMethod.1',
723                      'LastUpdatedAfter', 'LastUpdatedBefore'))
724        for do, dont in {
725            'BuyerEmail': toggle.union(['SellerOrderId']),
726            'SellerOrderId': toggle.union(['BuyerEmail']),
727        }.items():
728            if do in kw and any(i in dont for i in kw):
729                message = "Don't include {0} when specifying " \
730                          "{1}".format(' or '.join(dont), do)
731                raise AssertionError(message)
732        return self._post_request(request, kw, response)
733
734    @requires(['NextToken'])
735    @api_action('Orders', 6, 60)
736    def list_orders_by_next_token(self, request, response, **kw):
737        """Returns the next page of orders using the NextToken value
738           that was returned by your previous request to either
739           ListOrders or ListOrdersByNextToken.
740        """
741        return self._post_request(request, kw, response)
742
743    @requires(['AmazonOrderId'])
744    @structured_lists('AmazonOrderId.Id')
745    @api_action('Orders', 6, 60)
746    def get_order(self, request, response, **kw):
747        """Returns an order for each AmazonOrderId that you specify.
748        """
749        return self._post_request(request, kw, response)
750
751    @requires(['AmazonOrderId'])
752    @api_action('Orders', 30, 2)
753    def list_order_items(self, request, response, **kw):
754        """Returns order item information for an AmazonOrderId that
755           you specify.
756        """
757        return self._post_request(request, kw, response)
758
759    @requires(['NextToken'])
760    @api_action('Orders', 30, 2)
761    def list_order_items_by_next_token(self, request, response, **kw):
762        """Returns the next page of order items using the NextToken
763           value that was returned by your previous request to either
764           ListOrderItems or ListOrderItemsByNextToken.
765        """
766        return self._post_request(request, kw, response)
767
768    @api_action('Orders', 2, 300, 'GetServiceStatus')
769    def get_orders_service_status(self, request, response, **kw):
770        """Returns the operational status of the Orders API section.
771        """
772        return self._post_request(request, kw, response)
773
774    @requires(['MarketplaceId', 'Query'])
775    @api_action('Products', 20, 20)
776    def list_matching_products(self, request, response, **kw):
777        """Returns a list of products and their attributes, ordered
778           by relevancy, based on a search query that you specify.
779        """
780        return self._post_request(request, kw, response)
781
782    @requires(['MarketplaceId', 'ASINList'])
783    @structured_lists('ASINList.ASIN')
784    @api_action('Products', 20, 20)
785    def get_matching_product(self, request, response, **kw):
786        """Returns a list of products and their attributes, based on
787           a list of ASIN values that you specify.
788        """
789        return self._post_request(request, kw, response)
790
791    @requires(['MarketplaceId', 'IdType', 'IdList'])
792    @structured_lists('IdList.Id')
793    @api_action('Products', 20, 20)
794    def get_matching_product_for_id(self, request, response, **kw):
795        """Returns a list of products and their attributes, based on
796           a list of Product IDs that you specify.
797        """
798        return self._post_request(request, kw, response)
799
800    @requires(['MarketplaceId', 'SellerSKUList'])
801    @structured_lists('SellerSKUList.SellerSKU')
802    @api_action('Products', 20, 10, 'GetCompetitivePricingForSKU')
803    def get_competitive_pricing_for_sku(self, request, response, **kw):
804        """Returns the current competitive pricing of a product,
805           based on the SellerSKUs and MarketplaceId that you specify.
806        """
807        return self._post_request(request, kw, response)
808
809    @requires(['MarketplaceId', 'ASINList'])
810    @structured_lists('ASINList.ASIN')
811    @api_action('Products', 20, 10, 'GetCompetitivePricingForASIN')
812    def get_competitive_pricing_for_asin(self, request, response, **kw):
813        """Returns the current competitive pricing of a product,
814           based on the ASINs and MarketplaceId that you specify.
815        """
816        return self._post_request(request, kw, response)
817
818    @requires(['MarketplaceId', 'SellerSKUList'])
819    @structured_lists('SellerSKUList.SellerSKU')
820    @api_action('Products', 20, 5, 'GetLowestOfferListingsForSKU')
821    def get_lowest_offer_listings_for_sku(self, request, response, **kw):
822        """Returns the lowest price offer listings for a specific
823           product by item condition and SellerSKUs.
824        """
825        return self._post_request(request, kw, response)
826
827    @requires(['MarketplaceId', 'ASINList'])
828    @structured_lists('ASINList.ASIN')
829    @api_action('Products', 20, 5, 'GetLowestOfferListingsForASIN')
830    def get_lowest_offer_listings_for_asin(self, request, response, **kw):
831        """Returns the lowest price offer listings for a specific
832           product by item condition and ASINs.
833        """
834        return self._post_request(request, kw, response)
835
836    @requires(['MarketplaceId', 'SellerSKU'])
837    @api_action('Products', 20, 20, 'GetProductCategoriesForSKU')
838    def get_product_categories_for_sku(self, request, response, **kw):
839        """Returns the product categories that a SellerSKU belongs to.
840        """
841        return self._post_request(request, kw, response)
842
843    @requires(['MarketplaceId', 'ASIN'])
844    @api_action('Products', 20, 20, 'GetProductCategoriesForASIN')
845    def get_product_categories_for_asin(self, request, response, **kw):
846        """Returns the product categories that an ASIN belongs to.
847        """
848        return self._post_request(request, kw, response)
849
850    @api_action('Products', 2, 300, 'GetServiceStatus')
851    def get_products_service_status(self, request, response, **kw):
852        """Returns the operational status of the Products API section.
853        """
854        return self._post_request(request, kw, response)
855
856    @requires(['MarketplaceId', 'SellerSKUList'])
857    @structured_lists('SellerSKUList.SellerSKU')
858    @api_action('Products', 20, 10, 'GetMyPriceForSKU')
859    def get_my_price_for_sku(self, request, response, **kw):
860        """Returns pricing information for your own offer listings, based on SellerSKU.
861        """
862        return self._post_request(request, kw, response)
863
864    @requires(['MarketplaceId', 'ASINList'])
865    @structured_lists('ASINList.ASIN')
866    @api_action('Products', 20, 10, 'GetMyPriceForASIN')
867    def get_my_price_for_asin(self, request, response, **kw):
868        """Returns pricing information for your own offer listings, based on ASIN.
869        """
870        return self._post_request(request, kw, response)
871
872    @api_action('Sellers', 15, 60)
873    def list_marketplace_participations(self, request, response, **kw):
874        """Returns a list of marketplaces that the seller submitting
875           the request can sell in, and a list of participations that
876           include seller-specific information in that marketplace.
877        """
878        return self._post_request(request, kw, response)
879
880    @requires(['NextToken'])
881    @api_action('Sellers', 15, 60)
882    def list_marketplace_participations_by_next_token(self, request, response,
883                                                      **kw):
884        """Returns the next page of marketplaces and participations
885           using the NextToken value that was returned by your
886           previous request to either ListMarketplaceParticipations
887           or ListMarketplaceParticipationsByNextToken.
888        """
889        return self._post_request(request, kw, response)
890
891    @requires(['MarketplaceId'])
892    @api_action('Recommendations', 5, 2)
893    def get_last_updated_time_for_recommendations(self, request, response,
894                                                  **kw):
895        """Checks whether there are active recommendations for each category
896           for the given marketplace, and if there are, returns the time when
897           recommendations were last updated for each category.
898        """
899        return self._post_request(request, kw, response)
900
901    @requires(['MarketplaceId'])
902    @structured_lists('CategoryQueryList.CategoryQuery')
903    @api_action('Recommendations', 5, 2)
904    def list_recommendations(self, request, response, **kw):
905        """Returns your active recommendations for a specific category or for
906           all categories for a specific marketplace.
907        """
908        return self._post_request(request, kw, response)
909
910    @requires(['NextToken'])
911    @api_action('Recommendations', 5, 2)
912    def list_recommendations_by_next_token(self, request, response, **kw):
913        """Returns the next page of recommendations using the NextToken
914           parameter.
915        """
916        return self._post_request(request, kw, response)
917
918    @api_action('Recommendations', 2, 300, 'GetServiceStatus')
919    def get_recommendations_service_status(self, request, response, **kw):
920        """Returns the operational status of the Recommendations API section.
921        """
922        return self._post_request(request, kw, response)
923
924    @api_action('CustomerInfo', 15, 12)
925    def list_customers(self, request, response, **kw):
926        """Returns a list of customer accounts based on search criteria that
927           you specify.
928        """
929        return self._post_request(request, kw, response)
930
931    @requires(['NextToken'])
932    @api_action('CustomerInfo', 50, 3)
933    def list_customers_by_next_token(self, request, response, **kw):
934        """Returns the next page of customers using the NextToken parameter.
935        """
936        return self._post_request(request, kw, response)
937
938    @requires(['CustomerIdList'])
939    @structured_lists('CustomerIdList.CustomerId')
940    @api_action('CustomerInfo', 15, 12)
941    def get_customers_for_customer_id(self, request, response, **kw):
942        """Returns a list of customer accounts based on search criteria that
943           you specify.
944        """
945        return self._post_request(request, kw, response)
946
947    @api_action('CustomerInfo', 2, 300, 'GetServiceStatus')
948    def get_customerinfo_service_status(self, request, response, **kw):
949        """Returns the operational status of the Customer Information API
950           section.
951        """
952        return self._post_request(request, kw, response)
953
954    @requires(['DateRangeStart'])
955    @api_action('CartInfo', 15, 12)
956    def list_carts(self, request, response, **kw):
957        """Returns a list of shopping carts in your Webstore that were last
958           updated during the time range that you specify.
959        """
960        return self._post_request(request, kw, response)
961
962    @requires(['NextToken'])
963    @api_action('CartInfo', 50, 3)
964    def list_carts_by_next_token(self, request, response, **kw):
965        """Returns the next page of shopping carts using the NextToken
966           parameter.
967        """
968        return self._post_request(request, kw, response)
969
970    @requires(['CartIdList'])
971    @structured_lists('CartIdList.CartId')
972    @api_action('CartInfo', 15, 12)
973    def get_carts(self, request, response, **kw):
974        """Returns shopping carts based on the CartId values that you specify.
975        """
976        return self._post_request(request, kw, response)
977
978    @api_action('CartInfo', 2, 300, 'GetServiceStatus')
979    def get_cartinfo_service_status(self, request, response, **kw):
980        """Returns the operational status of the Cart Information API section.
981        """
982        return self._post_request(request, kw, response)
983
984    @requires(['MarketplaceId', 'Destination'])
985    @structured_objects('Destination', members=True)
986    @api_action('Subscriptions', 25, 0.5)
987    def register_destination(self, request, response, **kw):
988        """Specifies a new destination where you want to receive notifications.
989        """
990        return self._post_request(request, kw, response)
991
992    @requires(['MarketplaceId', 'Destination'])
993    @structured_objects('Destination', members=True)
994    @api_action('Subscriptions', 25, 0.5)
995    def deregister_destination(self, request, response, **kw):
996        """Removes an existing destination from the list of registered
997           destinations.
998        """
999        return self._post_request(request, kw, response)
1000
1001    @requires(['MarketplaceId'])
1002    @api_action('Subscriptions', 25, 0.5)
1003    def list_registered_destinations(self, request, response, **kw):
1004        """Lists all current destinations that you have registered.
1005        """
1006        return self._post_request(request, kw, response)
1007
1008    @requires(['MarketplaceId', 'Destination'])
1009    @structured_objects('Destination', members=True)
1010    @api_action('Subscriptions', 25, 0.5)
1011    def send_test_notification_to_destination(self, request, response, **kw):
1012        """Sends a test notification to an existing destination.
1013        """
1014        return self._post_request(request, kw, response)
1015
1016    @requires(['MarketplaceId', 'Subscription'])
1017    @structured_objects('Subscription', members=True)
1018    @api_action('Subscriptions', 25, 0.5)
1019    def create_subscription(self, request, response, **kw):
1020        """Creates a new subscription for the specified notification type
1021           and destination.
1022        """
1023        return self._post_request(request, kw, response)
1024
1025    @requires(['MarketplaceId', 'NotificationType', 'Destination'])
1026    @structured_objects('Destination', members=True)
1027    @api_action('Subscriptions', 25, 0.5)
1028    def get_subscription(self, request, response, **kw):
1029        """Gets the subscription for the specified notification type and
1030           destination.
1031        """
1032        return self._post_request(request, kw, response)
1033
1034    @requires(['MarketplaceId', 'NotificationType', 'Destination'])
1035    @structured_objects('Destination', members=True)
1036    @api_action('Subscriptions', 25, 0.5)
1037    def delete_subscription(self, request, response, **kw):
1038        """Deletes the subscription for the specified notification type and
1039           destination.
1040        """
1041        return self._post_request(request, kw, response)
1042
1043    @requires(['MarketplaceId'])
1044    @api_action('Subscriptions', 25, 0.5)
1045    def list_subscriptions(self, request, response, **kw):
1046        """Returns a list of all your current subscriptions.
1047        """
1048        return self._post_request(request, kw, response)
1049
1050    @requires(['MarketplaceId', 'Subscription'])
1051    @structured_objects('Subscription', members=True)
1052    @api_action('Subscriptions', 25, 0.5)
1053    def update_subscription(self, request, response, **kw):
1054        """Updates the subscription for the specified notification type and
1055           destination.
1056        """
1057        return self._post_request(request, kw, response)
1058
1059    @api_action('Subscriptions', 2, 300, 'GetServiceStatus')
1060    def get_subscriptions_service_status(self, request, response, **kw):
1061        """Returns the operational status of the Subscriptions API section.
1062        """
1063        return self._post_request(request, kw, response)
1064
1065    @requires(['AmazonOrderReferenceId', 'OrderReferenceAttributes'])
1066    @structured_objects('OrderReferenceAttributes')
1067    @api_action('OffAmazonPayments', 10, 1)
1068    def set_order_reference_details(self, request, response, **kw):
1069        """Sets order reference details such as the order total and a
1070           description for the order.
1071        """
1072        return self._post_request(request, kw, response)
1073
1074    @requires(['AmazonOrderReferenceId'])
1075    @api_action('OffAmazonPayments', 20, 2)
1076    def get_order_reference_details(self, request, response, **kw):
1077        """Returns details about the Order Reference object and its current
1078           state.
1079        """
1080        return self._post_request(request, kw, response)
1081
1082    @requires(['AmazonOrderReferenceId'])
1083    @api_action('OffAmazonPayments', 10, 1)
1084    def confirm_order_reference(self, request, response, **kw):
1085        """Confirms that the order reference is free of constraints and all
1086           required information has been set on the order reference.
1087        """
1088        return self._post_request(request, kw, response)
1089
1090    @requires(['AmazonOrderReferenceId'])
1091    @api_action('OffAmazonPayments', 10, 1)
1092    def cancel_order_reference(self, request, response, **kw):
1093        """Cancel an order reference; all authorizations associated with
1094           this order reference are also closed.
1095        """
1096        return self._post_request(request, kw, response)
1097
1098    @requires(['AmazonOrderReferenceId'])
1099    @api_action('OffAmazonPayments', 10, 1)
1100    def close_order_reference(self, request, response, **kw):
1101        """Confirms that an order reference has been fulfilled (fully
1102           or partially) and that you do not expect to create any new
1103           authorizations on this order reference.
1104        """
1105        return self._post_request(request, kw, response)
1106
1107    @requires(['AmazonOrderReferenceId', 'AuthorizationReferenceId',
1108               'AuthorizationAmount'])
1109    @structured_objects('AuthorizationAmount')
1110    @api_action('OffAmazonPayments', 10, 1)
1111    def authorize(self, request, response, **kw):
1112        """Reserves a specified amount against the payment method(s) stored in
1113           the order reference.
1114        """
1115        return self._post_request(request, kw, response)
1116
1117    @requires(['AmazonAuthorizationId'])
1118    @api_action('OffAmazonPayments', 20, 2)
1119    def get_authorization_details(self, request, response, **kw):
1120        """Returns the status of a particular authorization and the total
1121           amount captured on the authorization.
1122        """
1123        return self._post_request(request, kw, response)
1124
1125    @requires(['AmazonAuthorizationId', 'CaptureReferenceId', 'CaptureAmount'])
1126    @structured_objects('CaptureAmount')
1127    @api_action('OffAmazonPayments', 10, 1)
1128    def capture(self, request, response, **kw):
1129        """Captures funds from an authorized payment instrument.
1130        """
1131        return self._post_request(request, kw, response)
1132
1133    @requires(['AmazonCaptureId'])
1134    @api_action('OffAmazonPayments', 20, 2)
1135    def get_capture_details(self, request, response, **kw):
1136        """Returns the status of a particular capture and the total amount
1137           refunded on the capture.
1138        """
1139        return self._post_request(request, kw, response)
1140
1141    @requires(['AmazonAuthorizationId'])
1142    @api_action('OffAmazonPayments', 10, 1)
1143    def close_authorization(self, request, response, **kw):
1144        """Closes an authorization.
1145        """
1146        return self._post_request(request, kw, response)
1147
1148    @requires(['AmazonCaptureId', 'RefundReferenceId', 'RefundAmount'])
1149    @structured_objects('RefundAmount')
1150    @api_action('OffAmazonPayments', 10, 1)
1151    def refund(self, request, response, **kw):
1152        """Refunds a previously captured amount.
1153        """
1154        return self._post_request(request, kw, response)
1155
1156    @requires(['AmazonRefundId'])
1157    @api_action('OffAmazonPayments', 20, 2)
1158    def get_refund_details(self, request, response, **kw):
1159        """Returns the status of a particular refund.
1160        """
1161        return self._post_request(request, kw, response)
1162
1163    @api_action('OffAmazonPayments', 2, 300, 'GetServiceStatus')
1164    def get_offamazonpayments_service_status(self, request, response, **kw):
1165        """Returns the operational status of the Off-Amazon Payments API
1166           section.
1167        """
1168        return self._post_request(request, kw, response)
1169