From ab92f12534e0f77d755588036df0f9f8224a9857 Mon Sep 17 00:00:00 2001 From: Galen Rice Date: Tue, 6 Feb 2018 12:24:00 -0500 Subject: [PATCH 01/10] Merge recent changes in master to develop branch (#45) (#47) * Release for 0.8.0 (#42) * Added Finances API Feature * .setup.py: bump version to 0.7.5-dev0 * Split out request_description building from make_request() So that it can be tested more easily, and refactored * Split out building the initial params dict from make_request() So that it can be tested more easily * Add fake MWS credentials pytest fixture * test_service_status() should use the pytest fake credentials fixture * Add more pytest fixtures (access_key, secret_key, account_id, timestamp) * Add test for calc_request_description() * Split out calc_request_description() into more statements So that it is easier to debug * Fix calc_request_description - don't include leading ampersand * Don't do automated deployments via Travis (for the moment) * Update README.md badges * InboundShipments, next_token_action decorator, and some style cleanups. (#33) * Testing out git commits from VS Code * Reverting the test commit * Adding VS Code settings to gitignore. * Style fixes * MWS.enumerate_param deprecated: now using utils.enumerate_param and utils.enumerate_params * InboundShipments fleshed out; added `utils.next_token_action` decorator; deprecated separate methods for `...by_next_token()` * Bugfix, rename `_enumerate_param` to `enumerate_param` (no need for private) * Fix for next_token issues. * TravisCI flake8 complaining, fixes. * Minor flake8 complaint. * Hack to get flake8 to stop complaining. * Strip pylint disables to clear line length issue. * Correction to keyed params, now tests every item in values sequence to ensure all are dicts. * Add tests for param methods in utils. * Add test for next token decorator. * Adding 'InboundShipments' to `__all__` * Assigning response a default in __init__ for DictWrapper and DataWrapper * Unneeded line breaks removed + docstring formatting * Comment corrected. They're tuples, not sets. * Finances methods updated to use next_token_action decorator * Create .travis.yaml * Update .gitignore * Removing deploy code from local travis * Delete .travis.yaml * Pushed to 0.8.0-dev0 Recently added functionality to InboundShipments, as well as Finances API. These constitute feature additions with backwards compatibility, which calls for a minor version update. * Adding Python 3.6 category We are testing in 3.6 in Travis anyway. May as well include the note. * Specified master and develop branches for badges Ensured the badges for Travis and Codecov are pointing to the appropriate branches (used to be pointing to default, which was master in both cases). * Updated comments throughout module No substantial code changes, comment changes only. Also ensured all docstrings follow same format. * Fixed docstring formatting Also made slight update to docstring for ObjectDict to more clearly note what it does, vs what the original code did. * Fix for flake8 (trailing whitespace) * Fix for flake8 (trailing whitespace) * Bump to 0.8.0 (drop dev tag) for release * Bug: Incorrect use of `super` for back-compat Using the old-style `super` syntax to comply with Python 2.7 compatibility. Not revealed in tests, because current tests don't touch the APIs. Whoops! * Added back old object names in case needed Old names `object_dict` and `xml2dict` added back in case the old objects are being used directly by some users. To be removed in 1.0.0 release down the road. * Version bump for agent string. * Hi, howyadoin? * Format request url with str.format Remove old-style string formatting for sake of clarity. --- AUTHORS | 1 + mws/mws.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index e915e938..ae1f870c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,6 +1,7 @@ Current or previous core committers: * Paulo Alvarado +* Galen Rice Contributors (in alphabetical order): diff --git a/mws/mws.py b/mws/mws.py index ed254cb9..c83b621d 100644 --- a/mws/mws.py +++ b/mws/mws.py @@ -219,8 +219,13 @@ def make_request(self, extra_data, method="GET", **kwargs): params.update(extra_data) request_description = calc_request_description(params) signature = self.calc_signature(method, request_description) - url = '%s%s?%s&Signature=%s' % (self.domain, self.uri, request_description, quote(signature)) - headers = {'User-Agent': 'python-amazon-mws/0.0.1 (Language=Python)'} + url = "{domain}{uri}?{description}&Signature={signature}".format( + domain=self.domain, + uri=self.uri, + description=request_description, + signature=quote(signature), + ) + headers = {'User-Agent': 'python-amazon-mws/0.8.0 (Language=Python)'} headers.update(kwargs.get('extra_headers', {})) try: From 6c5f2772481754155e66ebf184458d5f2b5e76e0 Mon Sep 17 00:00:00 2001 From: Galen Rice Date: Mon, 12 Feb 2018 10:18:58 -0500 Subject: [PATCH 02/10] Refactor timestamp method. --- mws/mws.py | 9 +-------- mws/utils.py | 7 +++++++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mws/mws.py b/mws/mws.py index c83b621d..6e259fe7 100644 --- a/mws/mws.py +++ b/mws/mws.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -from time import gmtime, strftime import base64 import datetime import hashlib @@ -193,7 +192,7 @@ def get_params(self): 'AWSAccessKeyId': self.access_key, self.ACCOUNT_TYPE: self.account_id, 'SignatureVersion': '2', - 'Timestamp': self.get_timestamp(), + 'Timestamp': utils.get_utc_timestamp(), 'Version': self.version, 'SignatureMethod': 'HmacSHA256', } @@ -307,12 +306,6 @@ def calc_signature(self, method, request_description): ]) return base64.b64encode(hmac.new(self.secret_key.encode(), sig_data.encode(), hashlib.sha256).digest()) - def get_timestamp(self): - """ - Returns the current timestamp in proper format. - """ - return strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) - def enumerate_param(self, param, values): """ DEPRECATED. diff --git a/mws/utils.py b/mws/utils.py index f3d5f26c..b6ea569e 100644 --- a/mws/utils.py +++ b/mws/utils.py @@ -252,6 +252,13 @@ def _wrapped_func(self, *args, **kwargs): return _decorator +def get_utc_timestamp(): + """ + Returns the current UTC timestamp in ISO-8601 format. + """ + return datetime.datetime.utcnow().replace(microsecond=0).isoformat() + + # DEPRECATION: these are old names for these objects, which have been updated # to more idiomatic naming convention. Leaving these names in place in case # anyone is using the old object names. From b3df8e2cd1af0804392cf1b1571f76313d3d2fbd Mon Sep 17 00:00:00 2001 From: Galen Rice Date: Mon, 12 Feb 2018 10:20:04 -0500 Subject: [PATCH 03/10] Naming cleanups --- mws/mws.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/mws/mws.py b/mws/mws.py index 6e259fe7..362b0e6c 100644 --- a/mws/mws.py +++ b/mws/mws.py @@ -184,7 +184,7 @@ def __init__(self, access_key, secret_key, account_id, } raise MWSError(error_msg) - def get_params(self): + def get_default_params(self): """ Get the parameters required in all MWS requests """ @@ -214,7 +214,7 @@ def make_request(self, extra_data, method="GET", **kwargs): if isinstance(value, (datetime.datetime, datetime.date)): extra_data[key] = value.isoformat() - params = self.get_params() + params = self.get_default_params() params.update(extra_data) request_description = calc_request_description(params) signature = self.calc_signature(method, request_description) @@ -252,9 +252,9 @@ def make_request(self, extra_data, method="GET", **kwargs): except XMLError: parsed_response = DataWrapper(data, response.headers) - except HTTPError as e: - error = MWSError(str(e.response.text)) - error.response = e.response + except HTTPError as exc: + error = MWSError(str(exc.response.text)) + error.response = exc.response raise error # Store the response object in the parsed_response for quick access @@ -339,9 +339,9 @@ def submit_feed(self, feed, feed_type, marketplaceids=None, FeedType=feed_type, PurgeAndReplace=purge) data.update(utils.enumerate_param('MarketplaceIdList.Id.', marketplaceids)) - md = calc_md5(feed) + md5_hash = calc_md5(feed) return self.make_request(data, method="POST", body=feed, - extra_headers={'Content-MD5': md, 'Content-Type': content_type}) + extra_headers={'Content-MD5': md5_hash, 'Content-Type': content_type}) @utils.next_token_action('GetFeedSubmissionList') def get_feed_submission_list(self, feedids=None, max_count=None, feedtypes=None, @@ -868,7 +868,7 @@ def set_ship_from_address(self, address): for c in key_config} self.from_address = addr - def _parse_item_args(self, item_args, operation): + def parse_item_args(self, item_args, operation): """ Parses item arguments sent to create_inbound_shipment_plan, create_inbound_shipment, and update_inbound_shipment methods. @@ -969,7 +969,7 @@ def create_inbound_shipment_plan(self, items, country_code='US', subdivision_code = subdivision_code or None label_preference = label_preference or None - items = self._parse_item_args(items, 'CreateInboundShipmentPlan') + items = self.parse_item_args(items, 'CreateInboundShipmentPlan') if not self.from_address: raise MWSError(( "ShipFromAddress has not been set. " @@ -1010,7 +1010,7 @@ def create_inbound_shipment(self, shipment_id, shipment_name, if not items: raise MWSError("One or more `item` dict arguments required.") - items = self._parse_item_args(items, 'CreateInboundShipment') + items = self.parse_item_args(items, 'CreateInboundShipment') if not self.from_address: raise MWSError(( @@ -1066,7 +1066,7 @@ def update_inbound_shipment(self, shipment_id, shipment_name, # Parse item args if items: - items = self._parse_item_args(items, 'UpdateInboundShipment') + items = self.parse_item_args(items, 'UpdateInboundShipment') else: items = None From bf27e63ba557de7148ee3156e5a899bda37a835b Mon Sep 17 00:00:00 2001 From: Galen Rice Date: Mon, 12 Feb 2018 10:26:44 -0500 Subject: [PATCH 04/10] Single-source package version for use in agent string. --- mws/__init__.py | 2 ++ mws/mws.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/mws/__init__.py b/mws/__init__.py index 68463245..bbdf8a67 100644 --- a/mws/__init__.py +++ b/mws/__init__.py @@ -2,3 +2,5 @@ from __future__ import absolute_import from .mws import * # noqa: F401, F403 + +__version__ = '0.8.0' diff --git a/mws/mws.py b/mws/mws.py index 362b0e6c..e65f793f 100644 --- a/mws/mws.py +++ b/mws/mws.py @@ -12,6 +12,7 @@ from requests.exceptions import HTTPError from . import utils +from . import __version__ as package_version try: from urllib.parse import quote @@ -224,7 +225,7 @@ def make_request(self, extra_data, method="GET", **kwargs): description=request_description, signature=quote(signature), ) - headers = {'User-Agent': 'python-amazon-mws/0.8.0 (Language=Python)'} + headers = {'User-Agent': 'python-amazon-mws/{} (Language=Python)'.format(package_version)} headers.update(kwargs.get('extra_headers', {})) try: From 374c39f35c62b43bb463d6b54b3c3841840c0492 Mon Sep 17 00:00:00 2001 From: Galen Rice Date: Mon, 12 Feb 2018 11:57:07 -0500 Subject: [PATCH 05/10] MWS refactored, split into different modules. --- mws/__init__.py | 20 +- mws/apis/__init__.py | 23 + mws/apis/feeds.py | 85 +++ mws/apis/finances.py | 73 ++ mws/apis/inbound_shipments.py | 462 ++++++++++++ mws/apis/inventory.py | 50 ++ mws/{ => apis}/offamazonpayments.py | 8 +- mws/apis/orders.py | 79 ++ mws/apis/outbound_shipments.py | 20 + mws/apis/products.py | 130 ++++ mws/apis/recommendations.py | 55 ++ mws/apis/reports.py | 113 +++ mws/apis/sellers.py | 47 ++ mws/decorators.py | 26 + mws/mws.py | 1055 +-------------------------- mws/utils.py | 34 +- tests/test_next_token_decorator.py | 2 +- tests/test_utils.py | 3 +- 18 files changed, 1205 insertions(+), 1080 deletions(-) create mode 100644 mws/apis/__init__.py create mode 100644 mws/apis/feeds.py create mode 100644 mws/apis/finances.py create mode 100644 mws/apis/inbound_shipments.py create mode 100644 mws/apis/inventory.py rename mws/{ => apis}/offamazonpayments.py (98%) create mode 100644 mws/apis/orders.py create mode 100644 mws/apis/outbound_shipments.py create mode 100644 mws/apis/products.py create mode 100644 mws/apis/recommendations.py create mode 100644 mws/apis/reports.py create mode 100644 mws/apis/sellers.py create mode 100644 mws/decorators.py diff --git a/mws/__init__.py b/mws/__init__.py index bbdf8a67..d0686997 100644 --- a/mws/__init__.py +++ b/mws/__init__.py @@ -1,6 +1,20 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -from .mws import * # noqa: F401, F403 - -__version__ = '0.8.0' +from .mws import MWS, MWSError +from .apis import Feeds, Finances, InboundShipments, Inventory, OffAmazonPayments, Orders,\ + Products, Recommendations, Reports, Sellers +__all__ = [ + 'Feeds', + 'Finances', + 'InboundShipments', + 'Inventory', + 'MWS', + 'MWSError', + 'OffAmazonPayments', + 'Orders', + 'Products', + 'Recommendations', + 'Reports', + 'Sellers', +] diff --git a/mws/apis/__init__.py b/mws/apis/__init__.py new file mode 100644 index 00000000..5de7b34a --- /dev/null +++ b/mws/apis/__init__.py @@ -0,0 +1,23 @@ +from .feeds import Feeds +from .finances import Finances +from .inbound_shipments import InboundShipments +from .inventory import Inventory +from .offamazonpayments import OffAmazonPayments +from .orders import Orders +from .products import Products +from .recommendations import Recommendations +from .reports import Reports +from .sellers import Sellers + +__all__ = [ + 'Feeds', + 'Finances', + 'InboundShipments', + 'Inventory', + 'OffAmazonPayments', + 'Orders', + 'Products', + 'Recommendations', + 'Reports', + 'Sellers', +] diff --git a/mws/apis/feeds.py b/mws/apis/feeds.py new file mode 100644 index 00000000..0cd3ba63 --- /dev/null +++ b/mws/apis/feeds.py @@ -0,0 +1,85 @@ +""" +Amazon MWS Feeds API +""" +from __future__ import absolute_import +import warnings + +from ..mws import MWS +from .. import utils +from ..decorators import next_token_action + + +class Feeds(MWS): + """ + Amazon MWS Feeds API + """ + ACCOUNT_TYPE = "Merchant" + + NEXT_TOKEN_OPERATIONS = [ + 'GetFeedSubmissionList', + ] + + def submit_feed(self, feed, feed_type, marketplaceids=None, + content_type="text/xml", purge='false'): + """ + Uploads a feed ( xml or .tsv ) to the seller's inventory. + Can be used for creating/updating products on Amazon. + """ + data = dict(Action='SubmitFeed', + FeedType=feed_type, + PurgeAndReplace=purge) + data.update(utils.enumerate_param('MarketplaceIdList.Id.', marketplaceids)) + md5_hash = utils.calc_md5(feed) + return self.make_request(data, method="POST", body=feed, + extra_headers={'Content-MD5': md5_hash, 'Content-Type': content_type}) + + @next_token_action('GetFeedSubmissionList') + def get_feed_submission_list(self, feedids=None, max_count=None, feedtypes=None, + processingstatuses=None, fromdate=None, todate=None, + next_token=None): + """ + Returns a list of all feed submissions submitted in the previous 90 days. + That match the query parameters. + """ + + data = dict(Action='GetFeedSubmissionList', + MaxCount=max_count, + SubmittedFromDate=fromdate, + SubmittedToDate=todate,) + data.update(utils.enumerate_param('FeedSubmissionIdList.Id', feedids)) + data.update(utils.enumerate_param('FeedTypeList.Type.', feedtypes)) + data.update(utils.enumerate_param('FeedProcessingStatusList.Status.', processingstatuses)) + return self.make_request(data) + + def get_submission_list_by_next_token(self, token): + """ + Deprecated. + Use `get_feed_submission_list(next_token=token)` instead. + """ + # data = dict(Action='GetFeedSubmissionListByNextToken', NextToken=token) + # return self.make_request(data) + warnings.warn( + "Use `get_feed_submission_list(next_token=token)` instead.", + DeprecationWarning, + ) + return self.get_feed_submission_list(next_token=token) + + def get_feed_submission_count(self, feedtypes=None, processingstatuses=None, fromdate=None, todate=None): + data = dict(Action='GetFeedSubmissionCount', + SubmittedFromDate=fromdate, + SubmittedToDate=todate) + data.update(utils.enumerate_param('FeedTypeList.Type.', feedtypes)) + data.update(utils.enumerate_param('FeedProcessingStatusList.Status.', processingstatuses)) + return self.make_request(data) + + def cancel_feed_submissions(self, feedids=None, feedtypes=None, fromdate=None, todate=None): + data = dict(Action='CancelFeedSubmissions', + SubmittedFromDate=fromdate, + SubmittedToDate=todate) + data.update(utils.enumerate_param('FeedSubmissionIdList.Id.', feedids)) + data.update(utils.enumerate_param('FeedTypeList.Type.', feedtypes)) + return self.make_request(data) + + def get_feed_submission_result(self, feedid): + data = dict(Action='GetFeedSubmissionResult', FeedSubmissionId=feedid) + return self.make_request(data, rootkey='Message') diff --git a/mws/apis/finances.py b/mws/apis/finances.py new file mode 100644 index 00000000..7b204a6c --- /dev/null +++ b/mws/apis/finances.py @@ -0,0 +1,73 @@ +""" +Amazon MWS Finances API +""" +from __future__ import absolute_import +import warnings + +from ..mws import MWS +# from .. import utils +from ..decorators import next_token_action + + +class Finances(MWS): + """ + Amazon MWS Finances API + """ + URI = "/Finances/2015-05-01" + VERSION = "2015-05-01" + NS = '{https://mws.amazonservices.com/Finances/2015-05-01}' + NEXT_TOKEN_OPERATIONS = [ + 'ListFinancialEventGroups', + 'ListFinancialEvents', + ] + + @next_token_action('ListFinancialEventGroups') + def list_financial_event_groups(self, created_after=None, created_before=None, max_results=None, next_token=None): + """ + Returns a list of financial event groups + """ + data = dict( + Action='ListFinancialEventGroups', + FinancialEventGroupStartedAfter=created_after, + FinancialEventGroupStartedBefore=created_before, + MaxResultsPerPage=max_results, + ) + return self.make_request(data) + + def list_financial_event_groups_by_next_token(self, token): + """ + Deprecated. + Use `list_financial_event_groups(next_token=token)` instead. + """ + warnings.warn( + "Use `list_financial_event_groups(next_token=token)` instead.", + DeprecationWarning, + ) + return self.list_financial_event_groups(next_token=token) + + @next_token_action('ListFinancialEvents') + def list_financial_events(self, financial_event_group_id=None, amazon_order_id=None, posted_after=None, + posted_before=None, max_results=None, next_token=None): + """ + Returns financial events for a user-provided FinancialEventGroupId or AmazonOrderId + """ + data = dict( + Action='ListFinancialEvents', + FinancialEventGroupId=financial_event_group_id, + AmazonOrderId=amazon_order_id, + PostedAfter=posted_after, + PostedBefore=posted_before, + MaxResultsPerPage=max_results, + ) + return self.make_request(data) + + def list_financial_events_by_next_token(self, token): + """ + Deprecated. + Use `list_financial_events(next_token=token)` instead. + """ + warnings.warn( + "Use `list_financial_events(next_token=token)` instead.", + DeprecationWarning, + ) + return self.list_financial_events(next_token=token) diff --git a/mws/apis/inbound_shipments.py b/mws/apis/inbound_shipments.py new file mode 100644 index 00000000..8e147a87 --- /dev/null +++ b/mws/apis/inbound_shipments.py @@ -0,0 +1,462 @@ +""" +Amazon MWS FulfillmentInboundShipment API +""" +from __future__ import absolute_import +# import warnings + +from ..mws import MWS, MWSError +from .. import utils +from ..decorators import next_token_action + + +def parse_item_args(item_args, operation): + """ + Parses item arguments sent to create_inbound_shipment_plan, create_inbound_shipment, + and update_inbound_shipment methods. + + `item_args` is expected as an iterable containing dicts. + Each dict should have the following keys: + For `create_inbound_shipment_plan`: + REQUIRED: 'sku', 'quantity' + OPTIONAL: 'quantity_in_case', 'asin', 'condition' + Other operations: + REQUIRED: 'sku', 'quantity' + OPTIONAL: 'quantity_in_case' + If a required key is missing, throws MWSError. + All extra keys are ignored. + + Keys (above) are converted to the appropriate MWS key according to `key_config` (below) + based on the particular operation required. + """ + if not item_args: + raise MWSError("One or more `item` dict arguments required.") + + if operation == 'CreateInboundShipmentPlan': + # `key_config` composed of list of tuples, each tuple compose of: + # (input_key, output_key, is_required, default_value) + key_config = [ + ('sku', 'SellerSKU', True, None), + ('quantity', 'Quantity', True, None), + ('quantity_in_case', 'QuantityInCase', False, None), + ('asin', 'ASIN', False, None), + ('condition', 'Condition', False, None), + ] + # The expected MWS key for quantity is different for this operation. + # This ensures we use the right key later on. + quantity_key = 'Quantity' + else: + key_config = [ + ('sku', 'SellerSKU', True, None), + ('quantity', 'QuantityShipped', True, None), + ('quantity_in_case', 'QuantityInCase', False, None), + ] + quantity_key = 'QuantityShipped' + + items = [] + for item in item_args: + if not isinstance(item, dict): + raise MWSError("`item` argument must be a dict.") + if not all(k in item for k in [c[0] for c in key_config if c[2]]): + # Required keys of an item line missing + raise MWSError(( + "`item` dict missing required keys: {required}." + "\n- Optional keys: {optional}." + ).format( + required=', '.join([c[0] for c in key_config if c[2]]), + optional=', '.join([c[0] for c in key_config if not c[2]]), + )) + + # Get data from the item. + # Convert to str if present, or leave as None if missing + quantity = item.get('quantity') + if quantity is not None: + quantity = str(quantity) + + quantity_in_case = item.get('quantity_in_case') + if quantity_in_case is not None: + quantity_in_case = str(quantity_in_case) + + item_dict = { + 'SellerSKU': item.get('sku'), + quantity_key: quantity, + 'QuantityInCase': quantity_in_case, + } + item_dict.update({ + c[1]: item.get(c[0], c[3]) + for c in key_config + if c[0] not in ['sku', 'quantity', 'quantity_in_case'] + }) + items.append(item_dict) + + return items + + +class InboundShipments(MWS): + """ + Amazon MWS FulfillmentInboundShipment API + """ + URI = "/FulfillmentInboundShipment/2010-10-01" + VERSION = '2010-10-01' + NAMESPACE = '{http://mws.amazonaws.com/FulfillmentInboundShipment/2010-10-01/}' + NEXT_TOKEN_OPERATIONS = [ + 'ListInboundShipments', + 'ListInboundShipmentItems', + ] + SHIPMENT_STATUSES = ['WORKING', 'SHIPPED', 'CANCELLED'] + DEFAULT_SHIP_STATUS = 'WORKING' + LABEL_PREFERENCES = ['SELLER_LABEL', + 'AMAZON_LABEL_ONLY', + 'AMAZON_LABEL_PREFERRED'] + + def __init__(self, *args, **kwargs): + """ + Allow the addition of a from_address dict during object initialization. + kwarg "from_address" is caught and popped here, + then calls set_ship_from_address. + If empty or left out, empty dict is set by default. + """ + self.from_address = {} + addr = kwargs.pop('from_address', None) + if addr is not None: + self.from_address = self.set_ship_from_address(addr) + super(InboundShipments, self).__init__(*args, **kwargs) + + def set_ship_from_address(self, address): + """ + Verifies the structure of an address dictionary. + Once verified against the KEY_CONFIG, saves a parsed version + of that dictionary, ready to send to requests. + """ + # Clear existing + self.from_address = None + + if not address: + raise MWSError('Missing required `address` dict.') + if not isinstance(address, dict): + raise MWSError("`address` must be a dict") + + key_config = [ + # Tuples composed of: + # (input_key, output_key, is_required, default_value) + ('name', 'Name', True, None), + ('address_1', 'AddressLine1', True, None), + ('address_2', 'AddressLine2', False, None), + ('city', 'City', True, None), + ('district_or_county', 'DistrictOrCounty', False, None), + ('state_or_province', 'StateOrProvinceCode', False, None), + ('postal_code', 'PostalCode', False, None), + ('country', 'CountryCode', False, 'US'), + ] + + # Check if all REQUIRED keys in address exist: + if not all(k in address for k in + [c[0] for c in key_config if c[2]]): + # Required parts of address missing + raise MWSError(( + "`address` dict missing required keys: {required}." + "\n- Optional keys: {optional}." + ).format( + required=", ".join([c[0] for c in key_config if c[2]]), + optional=", ".join([c[0] for c in key_config if not c[2]]), + )) + + # Passed tests. Assign values + addr = {'ShipFromAddress.{}'.format(c[1]): address.get(c[0], c[3]) + for c in key_config} + self.from_address = addr + + def create_inbound_shipment_plan(self, items, country_code='US', + subdivision_code='', label_preference=''): + """ + Returns one or more inbound shipment plans, which provide the + information you need to create inbound shipments. + + At least one dictionary must be passed as `args`. Each dictionary + should contain the following keys: + REQUIRED: 'sku', 'quantity' + OPTIONAL: 'asin', 'condition', 'quantity_in_case' + + 'from_address' is required. Call 'set_ship_from_address' first before + using this operation. + """ + if not items: + raise MWSError("One or more `item` dict arguments required.") + subdivision_code = subdivision_code or None + label_preference = label_preference or None + + items = parse_item_args(items, 'CreateInboundShipmentPlan') + if not self.from_address: + raise MWSError(( + "ShipFromAddress has not been set. " + "Please use `.set_ship_from_address()` first." + )) + + data = dict( + Action='CreateInboundShipmentPlan', + ShipToCountryCode=country_code, + ShipToCountrySubdivisionCode=subdivision_code, + LabelPrepPreference=label_preference, + ) + data.update(self.from_address) + data.update(utils.enumerate_keyed_param( + 'InboundShipmentPlanRequestItems.member', items, + )) + return self.make_request(data, method="POST") + + def create_inbound_shipment(self, shipment_id, shipment_name, + destination, items, shipment_status='', + label_preference='', case_required=False, + box_contents_source=None): + """ + Creates an inbound shipment to Amazon's fulfillment network. + + At least one dictionary must be passed as `items`. Each dictionary + should contain the following keys: + REQUIRED: 'sku', 'quantity' + OPTIONAL: 'quantity_in_case' + + 'from_address' is required. Call 'set_ship_from_address' first before + using this operation. + """ + assert isinstance(shipment_id, str), "`shipment_id` must be a string." + assert isinstance(shipment_name, str), "`shipment_name` must be a string." + assert isinstance(destination, str), "`destination` must be a string." + + if not items: + raise MWSError("One or more `item` dict arguments required.") + + items = parse_item_args(items, 'CreateInboundShipment') + + if not self.from_address: + raise MWSError(( + "ShipFromAddress has not been set. " + "Please use `.set_ship_from_address()` first." + )) + from_address = self.from_address + from_address = {'InboundShipmentHeader.{}'.format(k): v + for k, v in from_address.items()} + + if shipment_status not in self.SHIPMENT_STATUSES: + # Status is required for `create` request. + # Set it to default. + shipment_status = self.DEFAULT_SHIP_STATUS + + if label_preference not in self.LABEL_PREFERENCES: + # Label preference not required. Set to None + label_preference = None + + # Explict True/False for case_required, + # written as the strings MWS expects. + case_required = 'true' if case_required else 'false' + + data = { + 'Action': 'CreateInboundShipment', + 'ShipmentId': shipment_id, + 'InboundShipmentHeader.ShipmentName': shipment_name, + 'InboundShipmentHeader.DestinationFulfillmentCenterId': destination, + 'InboundShipmentHeader.LabelPrepPreference': label_preference, + 'InboundShipmentHeader.AreCasesRequired': case_required, + 'InboundShipmentHeader.ShipmentStatus': shipment_status, + 'InboundShipmentHeader.IntendedBoxContentsSource': box_contents_source, + } + data.update(from_address) + data.update(utils.enumerate_keyed_param( + 'InboundShipmentItems.member', items, + )) + return self.make_request(data, method="POST") + + def update_inbound_shipment(self, shipment_id, shipment_name, + destination, items=None, shipment_status='', + label_preference='', case_required=False, + box_contents_source=None): + """ + Updates an existing inbound shipment in Amazon FBA. + 'from_address' is required. Call 'set_ship_from_address' first before + using this operation. + """ + # Assert these are strings, error out if not. + assert isinstance(shipment_id, str), "`shipment_id` must be a string." + assert isinstance(shipment_name, str), "`shipment_name` must be a string." + assert isinstance(destination, str), "`destination` must be a string." + + # Parse item args + if items: + items = parse_item_args(items, 'UpdateInboundShipment') + else: + items = None + + # Raise exception if no from_address has been set prior to calling + if not self.from_address: + raise MWSError(( + "ShipFromAddress has not been set. " + "Please use `.set_ship_from_address()` first." + )) + # Assemble the from_address using operation-specific header + from_address = self.from_address + from_address = {'InboundShipmentHeader.{}'.format(k): v + for k, v in from_address.items()} + + if shipment_status not in self.SHIPMENT_STATUSES: + # Passed shipment status is an invalid choice. + # Remove it from this request by setting it to None. + shipment_status = None + + if label_preference not in self.LABEL_PREFERENCES: + # Passed label preference is an invalid choice. + # Remove it from this request by setting it to None. + label_preference = None + + case_required = 'true' if case_required else 'false' + + data = { + 'Action': 'UpdateInboundShipment', + 'ShipmentId': shipment_id, + 'InboundShipmentHeader.ShipmentName': shipment_name, + 'InboundShipmentHeader.DestinationFulfillmentCenterId': destination, + 'InboundShipmentHeader.LabelPrepPreference': label_preference, + 'InboundShipmentHeader.AreCasesRequired': case_required, + 'InboundShipmentHeader.ShipmentStatus': shipment_status, + 'InboundShipmentHeader.IntendedBoxContentsSource': box_contents_source, + } + data.update(from_address) + if items: + # Update with an items paramater only if they exist. + data.update(utils.enumerate_keyed_param( + 'InboundShipmentItems.member', items, + )) + return self.make_request(data, method="POST") + + def get_prep_instructions_for_sku(self, skus=None, country_code=None): + """ + Returns labeling requirements and item preparation instructions + to help you prepare items for an inbound shipment. + """ + country_code = country_code or 'US' + skus = skus or [] + + # 'skus' should be a unique list, or there may be an error returned. + skus = utils.unique_list_order_preserved(skus) + + data = dict( + Action='GetPrepInstructionsForSKU', + ShipToCountryCode=country_code, + ) + data.update(utils.enumerate_params({ + 'SellerSKUList.ID.': skus, + })) + return self.make_request(data, method="POST") + + def get_prep_instructions_for_asin(self, asins=None, country_code=None): + """ + Returns item preparation instructions to help with + item sourcing decisions. + """ + country_code = country_code or 'US' + asins = asins or [] + + # 'asins' should be a unique list, or there may be an error returned. + asins = utils.unique_list_order_preserved(asins) + + data = dict( + Action='GetPrepInstructionsForASIN', + ShipToCountryCode=country_code, + ) + data.update(utils.enumerate_params({ + 'ASINList.ID.': asins, + })) + return self.make_request(data, method="POST") + + def get_package_labels(self, shipment_id, num_packages, page_type=None): + """ + Returns PDF document data for printing package labels for + an inbound shipment. + """ + data = dict( + Action='GetPackageLabels', + ShipmentId=shipment_id, + PageType=page_type, + NumberOfPackages=str(num_packages), + ) + return self.make_request(data, method="POST") + + def get_transport_content(self, shipment_id): + """ + Returns current transportation information about an + inbound shipment. + """ + data = dict( + Action='GetTransportContent', + ShipmentId=shipment_id + ) + return self.make_request(data, method="POST") + + def estimate_transport_request(self, shipment_id): + """ + Requests an estimate of the shipping cost for an inbound shipment. + """ + data = dict( + Action='EstimateTransportRequest', + ShipmentId=shipment_id, + ) + return self.make_request(data, method="POST") + + def void_transport_request(self, shipment_id): + """ + Voids a previously-confirmed request to ship your inbound shipment + using an Amazon-partnered carrier. + """ + data = dict( + Action='VoidTransportRequest', + ShipmentId=shipment_id + ) + return self.make_request(data, method="POST") + + def get_bill_of_lading(self, shipment_id): + """ + Returns PDF document data for printing a bill of lading + for an inbound shipment. + """ + data = dict( + Action='GetBillOfLading', + ShipmentId=shipment_id, + ) + return self.make_request(data, "POST") + + @next_token_action('ListInboundShipments') + def list_inbound_shipments(self, shipment_ids=None, shipment_statuses=None, + last_updated_after=None, last_updated_before=None,): + """ + Returns list of shipments based on statuses, IDs, and/or + before/after datetimes. + """ + last_updated_after = utils.dt_iso_or_none(last_updated_after) + last_updated_before = utils.dt_iso_or_none(last_updated_before) + + data = dict( + Action='ListInboundShipments', + LastUpdatedAfter=last_updated_after, + LastUpdatedBefore=last_updated_before, + ) + data.update(utils.enumerate_params({ + 'ShipmentStatusList.member.': shipment_statuses, + 'ShipmentIdList.member.': shipment_ids, + })) + return self.make_request(data, method="POST") + + @next_token_action('ListInboundShipmentItems') + def list_inbound_shipment_items(self, shipment_id=None, last_updated_after=None, + last_updated_before=None,): + """ + Returns list of items within inbound shipments and/or + before/after datetimes. + """ + last_updated_after = utils.dt_iso_or_none(last_updated_after) + last_updated_before = utils.dt_iso_or_none(last_updated_before) + + data = dict( + Action='ListInboundShipmentItems', + ShipmentId=shipment_id, + LastUpdatedAfter=last_updated_after, + LastUpdatedBefore=last_updated_before, + ) + return self.make_request(data, method="POST") diff --git a/mws/apis/inventory.py b/mws/apis/inventory.py new file mode 100644 index 00000000..8761bfbf --- /dev/null +++ b/mws/apis/inventory.py @@ -0,0 +1,50 @@ +""" +Amazon MWS Inventory Fulfillment API +""" +from __future__ import absolute_import +import warnings + +from ..mws import MWS +from .. import utils +from ..decorators import next_token_action + + +class Inventory(MWS): + """ + Amazon MWS Inventory Fulfillment API + """ + + URI = '/FulfillmentInventory/2010-10-01' + VERSION = '2010-10-01' + NAMESPACE = "{http://mws.amazonaws.com/FulfillmentInventory/2010-10-01}" + NEXT_TOKEN_OPERATIONS = [ + 'ListInventorySupply', + ] + + @next_token_action('ListInventorySupply') + def list_inventory_supply(self, skus=(), datetime_=None, + response_group='Basic', next_token=None): + """ + Returns information on available inventory + """ + + data = dict( + Action='ListInventorySupply', + QueryStartDateTime=datetime_, + ResponseGroup=response_group, + ) + data.update(utils.enumerate_param('SellerSkus.member.', skus)) + return self.make_request(data, "POST") + + def list_inventory_supply_by_next_token(self, token): + """ + Deprecated. + Use `list_inventory_supply(next_token=token)` instead. + """ + # data = dict(Action='ListInventorySupplyByNextToken', NextToken=token) + # return self.make_request(data, "POST") + warnings.warn( + "Use `list_inventory_supply(next_token=token)` instead.", + DeprecationWarning, + ) + return self.list_inventory_supply(next_token=token) diff --git a/mws/offamazonpayments.py b/mws/apis/offamazonpayments.py similarity index 98% rename from mws/offamazonpayments.py rename to mws/apis/offamazonpayments.py index 2b293742..9166d46d 100644 --- a/mws/offamazonpayments.py +++ b/mws/apis/offamazonpayments.py @@ -1,10 +1,13 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -from .mws import MWS +from ..mws import MWS class OffAmazonPayments(MWS): + """ + Amazon OffAmazonPayments Sandbox API. + """ SANDBOX_URI = "/OffAmazonPayments_Sandbox/2013-01-01/" URI = "/OffAmazonPayments/2013-01-01/" VERSION = "2013-01-01" @@ -137,8 +140,7 @@ def set_order_reference_details(self, order_ref, order_total, params = { "OrderReferenceAttributes.OrderTotal.Amount": str(order_total), "OrderReferenceAttributes.OrderTotal.CurrencyCode": currency, - "OrderReferenceAttributes.SellerOrderAttributes.SellerOrderId": str( - order_id), + "OrderReferenceAttributes.SellerOrderAttributes.SellerOrderId": str(order_id), "OrderReferenceAttributes.SellerOrderAttributes.StoreName": store_name, "OrderReferenceAttributes.SellerNote": note, } diff --git a/mws/apis/orders.py b/mws/apis/orders.py new file mode 100644 index 00000000..ffc17555 --- /dev/null +++ b/mws/apis/orders.py @@ -0,0 +1,79 @@ +""" +Amazon Orders API +""" +from __future__ import absolute_import +import warnings + +from ..mws import MWS +from .. import utils +from ..decorators import next_token_action + + +class Orders(MWS): + """ + Amazon Orders API + """ + URI = "/Orders/2013-09-01" + VERSION = "2013-09-01" + NAMESPACE = '{https://mws.amazonservices.com/Orders/2013-09-01}' + NEXT_TOKEN_OPERATIONS = [ + 'ListOrders', + 'ListOrderItems', + ] + + @next_token_action('ListOrders') + def list_orders(self, marketplaceids=None, created_after=None, created_before=None, + lastupdatedafter=None, lastupdatedbefore=None, orderstatus=(), + fulfillment_channels=(), payment_methods=(), buyer_email=None, + seller_orderid=None, max_results='100', next_token=None): + + data = dict(Action='ListOrders', + CreatedAfter=created_after, + CreatedBefore=created_before, + LastUpdatedAfter=lastupdatedafter, + LastUpdatedBefore=lastupdatedbefore, + BuyerEmail=buyer_email, + SellerOrderId=seller_orderid, + MaxResultsPerPage=max_results, + ) + data.update(utils.enumerate_param('OrderStatus.Status.', orderstatus)) + data.update(utils.enumerate_param('MarketplaceId.Id.', marketplaceids)) + data.update(utils.enumerate_param('FulfillmentChannel.Channel.', fulfillment_channels)) + data.update(utils.enumerate_param('PaymentMethod.Method.', payment_methods)) + return self.make_request(data) + + def list_orders_by_next_token(self, token): + """ + Deprecated. + Use `list_orders(next_token=token)` instead. + """ + # data = dict(Action='ListOrdersByNextToken', NextToken=token) + # return self.make_request(data) + warnings.warn( + "Use `list_orders(next_token=token)` instead.", + DeprecationWarning, + ) + return self.list_orders(next_token=token) + + def get_order(self, amazon_order_ids): + data = dict(Action='GetOrder') + data.update(utils.enumerate_param('AmazonOrderId.Id.', amazon_order_ids)) + return self.make_request(data) + + @next_token_action('ListOrderItems') + def list_order_items(self, amazon_order_id=None, next_token=None): + data = dict(Action='ListOrderItems', AmazonOrderId=amazon_order_id) + return self.make_request(data) + + def list_order_items_by_next_token(self, token): + """ + Deprecated. + Use `list_order_items(next_token=token)` instead. + """ + # data = dict(Action='ListOrderItemsByNextToken', NextToken=token) + # return self.make_request(data) + warnings.warn( + "Use `list_order_items(next_token=token)` instead.", + DeprecationWarning, + ) + return self.list_order_items(next_token=token) diff --git a/mws/apis/outbound_shipments.py b/mws/apis/outbound_shipments.py new file mode 100644 index 00000000..64756efd --- /dev/null +++ b/mws/apis/outbound_shipments.py @@ -0,0 +1,20 @@ +""" +Amazon MWS Fulfillment Outbound Shipments API +""" +from __future__ import absolute_import +# import warnings + +from ..mws import MWS +# from .. import utils + + +class OutboundShipments(MWS): + """ + Amazon MWS Fulfillment Outbound Shipments API + """ + URI = "/FulfillmentOutboundShipment/2010-10-01" + VERSION = "2010-10-01" + NEXT_TOKEN_OPERATIONS = [ + 'ListAllFulfillmentOrders', + ] + # TODO: Complete this class section diff --git a/mws/apis/products.py b/mws/apis/products.py new file mode 100644 index 00000000..1a4d8d27 --- /dev/null +++ b/mws/apis/products.py @@ -0,0 +1,130 @@ +""" +Amazon MWS Products API +""" +from __future__ import absolute_import +# import warnings + +from ..mws import MWS +from .. import utils + + +class Products(MWS): + """ + Amazon MWS Products API + """ + URI = '/Products/2011-10-01' + VERSION = '2011-10-01' + NAMESPACE = '{http://mws.amazonservices.com/schema/Products/2011-10-01}' + # NEXT_TOKEN_OPERATIONS = [] + + def list_matching_products(self, marketplaceid, query, contextid=None): + """ + Returns a list of products and their attributes, ordered by + relevancy, based on a search query that you specify. + Your search query can be a phrase that describes the product + or it can be a product identifier such as a UPC, EAN, ISBN, or JAN. + """ + data = dict(Action='ListMatchingProducts', + MarketplaceId=marketplaceid, + Query=query, + QueryContextId=contextid) + return self.make_request(data) + + def get_matching_product(self, marketplaceid, asins): + """ + Returns a list of products and their attributes, based on a list of + ASIN values that you specify. + """ + data = dict(Action='GetMatchingProduct', MarketplaceId=marketplaceid) + data.update(utils.enumerate_param('ASINList.ASIN.', asins)) + return self.make_request(data) + + def get_matching_product_for_id(self, marketplaceid, type_, ids): + """ + Returns a list of products and their attributes, based on a list of + product identifier values (ASIN, SellerSKU, UPC, EAN, ISBN, GCID and JAN) + The identifier type is case sensitive. + Added in Fourth Release, API version 2011-10-01 + """ + data = dict(Action='GetMatchingProductForId', + MarketplaceId=marketplaceid, + IdType=type_) + + data.update(utils.enumerate_param('IdList.Id.', ids)) + return self.make_request(data) + + def get_competitive_pricing_for_sku(self, marketplaceid, skus): + """ + Returns the current competitive pricing of a product, + based on the SellerSKU and MarketplaceId that you specify. + """ + data = dict(Action='GetCompetitivePricingForSKU', MarketplaceId=marketplaceid) + data.update(utils.enumerate_param('SellerSKUList.SellerSKU.', skus)) + return self.make_request(data) + + def get_competitive_pricing_for_asin(self, marketplaceid, asins): + """ + Returns the current competitive pricing of a product, + based on the ASIN and MarketplaceId that you specify. + """ + data = dict(Action='GetCompetitivePricingForASIN', MarketplaceId=marketplaceid) + data.update(utils.enumerate_param('ASINList.ASIN.', asins)) + return self.make_request(data) + + def get_lowest_offer_listings_for_sku(self, marketplaceid, skus, condition="Any", excludeme="False"): + data = dict(Action='GetLowestOfferListingsForSKU', + MarketplaceId=marketplaceid, + ItemCondition=condition, + ExcludeMe=excludeme) + data.update(utils.enumerate_param('SellerSKUList.SellerSKU.', skus)) + return self.make_request(data) + + def get_lowest_offer_listings_for_asin(self, marketplaceid, asins, condition="Any", excludeme="False"): + data = dict(Action='GetLowestOfferListingsForASIN', + MarketplaceId=marketplaceid, + ItemCondition=condition, + ExcludeMe=excludeme) + data.update(utils.enumerate_param('ASINList.ASIN.', asins)) + return self.make_request(data) + + def get_lowest_priced_offers_for_sku(self, marketplaceid, sku, condition="New", excludeme="False"): + data = dict(Action='GetLowestPricedOffersForSKU', + MarketplaceId=marketplaceid, + SellerSKU=sku, + ItemCondition=condition, + ExcludeMe=excludeme) + return self.make_request(data) + + def get_lowest_priced_offers_for_asin(self, marketplaceid, asin, condition="New", excludeme="False"): + data = dict(Action='GetLowestPricedOffersForASIN', + MarketplaceId=marketplaceid, + ASIN=asin, + ItemCondition=condition, + ExcludeMe=excludeme) + return self.make_request(data) + + def get_product_categories_for_sku(self, marketplaceid, sku): + data = dict(Action='GetProductCategoriesForSKU', + MarketplaceId=marketplaceid, + SellerSKU=sku) + return self.make_request(data) + + def get_product_categories_for_asin(self, marketplaceid, asin): + data = dict(Action='GetProductCategoriesForASIN', + MarketplaceId=marketplaceid, + ASIN=asin) + return self.make_request(data) + + def get_my_price_for_sku(self, marketplaceid, skus, condition=None): + data = dict(Action='GetMyPriceForSKU', + MarketplaceId=marketplaceid, + ItemCondition=condition) + data.update(utils.enumerate_param('SellerSKUList.SellerSKU.', skus)) + return self.make_request(data) + + def get_my_price_for_asin(self, marketplaceid, asins, condition=None): + data = dict(Action='GetMyPriceForASIN', + MarketplaceId=marketplaceid, + ItemCondition=condition) + data.update(utils.enumerate_param('ASINList.ASIN.', asins)) + return self.make_request(data) diff --git a/mws/apis/recommendations.py b/mws/apis/recommendations.py new file mode 100644 index 00000000..0c3fbaf4 --- /dev/null +++ b/mws/apis/recommendations.py @@ -0,0 +1,55 @@ +""" +Amazon MWS Recommendations API +""" +from __future__ import absolute_import +import warnings + +from ..mws import MWS +# from .. import utils +from ..decorators import next_token_action + + +class Recommendations(MWS): + """ + Amazon MWS Recommendations API + """ + URI = '/Recommendations/2013-04-01' + VERSION = '2013-04-01' + NAMESPACE = "{https://mws.amazonservices.com/Recommendations/2013-04-01}" + NEXT_TOKEN_OPERATIONS = [ + "ListRecommendations", + ] + + def get_last_updated_time_for_recommendations(self, marketplaceid): + """ + Checks whether there are active recommendations for each category for the given marketplace, and if there are, + returns the time when recommendations were last updated for each category. + """ + data = dict(Action='GetLastUpdatedTimeForRecommendations', + MarketplaceId=marketplaceid) + return self.make_request(data, "POST") + + @next_token_action('ListRecommendations') + def list_recommendations(self, marketplaceid=None, + recommendationcategory=None, next_token=None): + """ + Returns your active recommendations for a specific category or for all categories for a specific marketplace. + """ + data = dict(Action="ListRecommendations", + MarketplaceId=marketplaceid, + RecommendationCategory=recommendationcategory) + return self.make_request(data, "POST") + + def list_recommendations_by_next_token(self, token): + """ + Deprecated. + Use `list_recommendations(next_token=token)` instead. + """ + # data = dict(Action="ListRecommendationsByNextToken", + # NextToken=token) + # return self.make_request(data, "POST") + warnings.warn( + "Use `list_recommendations(next_token=token)` instead.", + DeprecationWarning, + ) + return self.list_recommendations(next_token=token) diff --git a/mws/apis/reports.py b/mws/apis/reports.py new file mode 100644 index 00000000..bf66fd4b --- /dev/null +++ b/mws/apis/reports.py @@ -0,0 +1,113 @@ +""" +Amazon MWS Reports API +""" +from __future__ import absolute_import +import warnings + +import mws +from .. import utils +from ..decorators import next_token_action + + +class Reports(mws.MWS): + """ + Amazon MWS Reports API + """ + ACCOUNT_TYPE = "Merchant" + NEXT_TOKEN_OPERATIONS = [ + 'GetReportRequestList', + 'GetReportScheduleList', + ] + + # * REPORTS * # + + def get_report(self, report_id): + data = dict(Action='GetReport', ReportId=report_id) + return self.make_request(data) + + def get_report_count(self, report_types=(), acknowledged=None, fromdate=None, todate=None): + data = dict(Action='GetReportCount', + Acknowledged=acknowledged, + AvailableFromDate=fromdate, + AvailableToDate=todate) + data.update(utils.enumerate_param('ReportTypeList.Type.', report_types)) + return self.make_request(data) + + @next_token_action('GetReportList') + def get_report_list(self, requestids=(), max_count=None, types=(), acknowledged=None, + fromdate=None, todate=None, next_token=None): + data = dict(Action='GetReportList', + Acknowledged=acknowledged, + AvailableFromDate=fromdate, + AvailableToDate=todate, + MaxCount=max_count) + data.update(utils.enumerate_param('ReportRequestIdList.Id.', requestids)) + data.update(utils.enumerate_param('ReportTypeList.Type.', types)) + return self.make_request(data) + + def get_report_list_by_next_token(self, token): + """ + Deprecated. + Use `get_report_list(next_token=token)` instead. + """ + # data = dict(Action='GetReportListByNextToken', NextToken=token) + # return self.make_request(data) + warnings.warn( + "Use `get_report_list(next_token=token)` instead.", + DeprecationWarning, + ) + return self.get_report_list(next_token=token) + + def get_report_request_count(self, report_types=(), processingstatuses=(), + fromdate=None, todate=None): + data = dict(Action='GetReportRequestCount', + RequestedFromDate=fromdate, + RequestedToDate=todate) + data.update(utils.enumerate_param('ReportTypeList.Type.', report_types)) + data.update(utils.enumerate_param('ReportProcessingStatusList.Status.', processingstatuses)) + return self.make_request(data) + + @next_token_action('GetReportRequestList') + def get_report_request_list(self, requestids=(), types=(), processingstatuses=(), + max_count=None, fromdate=None, todate=None, next_token=None): + data = dict(Action='GetReportRequestList', + MaxCount=max_count, + RequestedFromDate=fromdate, + RequestedToDate=todate) + data.update(utils.enumerate_param('ReportRequestIdList.Id.', requestids)) + data.update(utils.enumerate_param('ReportTypeList.Type.', types)) + data.update(utils.enumerate_param('ReportProcessingStatusList.Status.', processingstatuses)) + return self.make_request(data) + + def get_report_request_list_by_next_token(self, token): + """ + Deprecated. + Use `get_report_request_list(next_token=token)` instead. + """ + # data = dict(Action='GetReportRequestListByNextToken', NextToken=token) + # return self.make_request(data) + warnings.warn( + "Use `get_report_request_list(next_token=token)` instead.", + DeprecationWarning, + ) + return self.get_report_request_list(next_token=token) + + def request_report(self, report_type, start_date=None, end_date=None, marketplaceids=()): + data = dict(Action='RequestReport', + ReportType=report_type, + StartDate=start_date, + EndDate=end_date) + data.update(utils.enumerate_param('MarketplaceIdList.Id.', marketplaceids)) + return self.make_request(data) + + # * ReportSchedule * # + + def get_report_schedule_list(self, types=()): + data = dict(Action='GetReportScheduleList') + data.update(utils.enumerate_param('ReportTypeList.Type.', types)) + return self.make_request(data) + + def get_report_schedule_count(self, types=()): + data = dict(Action='GetReportScheduleCount') + data.update(utils.enumerate_param('ReportTypeList.Type.', types)) + return self.make_request(data) diff --git a/mws/apis/sellers.py b/mws/apis/sellers.py new file mode 100644 index 00000000..b49963fa --- /dev/null +++ b/mws/apis/sellers.py @@ -0,0 +1,47 @@ +""" +Amazon MWS Sellers API +""" +from __future__ import absolute_import +import warnings + +from ..mws import MWS +# from .. import utils +from ..decorators import next_token_action + + +class Sellers(MWS): + """ + Amazon MWS Sellers API + """ + URI = '/Sellers/2011-07-01' + VERSION = '2011-07-01' + NAMESPACE = '{http://mws.amazonservices.com/schema/Sellers/2011-07-01}' + NEXT_TOKEN_OPERATIONS = [ + 'ListMarketplaceParticipations', + ] + + @next_token_action('ListMarketplaceParticipations') + def list_marketplace_participations(self, next_token=None): + """ + Returns a list of marketplaces a seller can participate in and + a list of participations that include seller-specific information in that marketplace. + The operation returns only those marketplaces where the seller's account is + in an active state. + + Run with `next_token` kwarg to call related "ByNextToken" action. + """ + data = dict(Action='ListMarketplaceParticipations') + return self.make_request(data) + + def list_marketplace_participations_by_next_token(self, token): + """ + Deprecated. + Use `list_marketplace_participations(next_token=token)` instead. + """ + # data = dict(Action='ListMarketplaceParticipations', NextToken=token) + # return self.make_request(data) + warnings.warn( + "Use `list_marketplace_participations(next_token=token)` instead.", + DeprecationWarning, + ) + return self.list_marketplace_participations(next_token=token) diff --git a/mws/decorators.py b/mws/decorators.py new file mode 100644 index 00000000..9b659f1d --- /dev/null +++ b/mws/decorators.py @@ -0,0 +1,26 @@ +""" +Decorator methods for MWS module. +""" +from __future__ import absolute_import +from functools import wraps + + +def next_token_action(action_name): + """ + Decorator that designates an action as having a "...ByNextToken" associated request. + Checks for a `next_token` kwargs in the request and, if present, redirects the call + to `action_by_next_token` using the given `action_name`. + + Only the `next_token` kwarg is consumed by the "next" call: + all other args and kwargs are ignored and not required. + """ + def _decorator(request_func): + @wraps(request_func) + def _wrapped_func(self, *args, **kwargs): + next_token = kwargs.pop('next_token', None) + if next_token is not None: + # Token captured: run the "next" action. + return self.action_by_next_token(action_name, next_token) + return request_func(self, *args, **kwargs) + return _wrapped_func + return _decorator diff --git a/mws/mws.py b/mws/mws.py index e65f793f..3b65e25b 100644 --- a/mws/mws.py +++ b/mws/mws.py @@ -12,7 +12,6 @@ from requests.exceptions import HTTPError from . import utils -from . import __version__ as package_version try: from urllib.parse import quote @@ -24,18 +23,7 @@ from xml.parsers.expat import ExpatError as XMLError -__all__ = [ - 'Feeds', - 'Inventory', - 'InboundShipments', - 'MWSError', - 'Reports', - 'Orders', - 'Products', - 'Recommendations', - 'Sellers', - 'Finances', -] +__version__ = '0.8.0' # See https://images-na.ssl-images-amazon.com/images/G/01/mwsportal/doc/en_US/bde/MWSDeveloperGuide._V357736853_.pdf # page 8 @@ -65,16 +53,11 @@ class MWSError(Exception): response = None -def calc_md5(string): +def calc_request_description(params): """ - Calculates the MD5 encryption for the given string + Returns a flatted string with the request description, built from the params dict. + Entries are escaped with urllib quote method, formatted as "key=value", and joined with "&". """ - md5_hash = hashlib.md5() - md5_hash.update(string) - return base64.b64encode(md5_hash.digest()).strip(b'\n') - - -def calc_request_description(params): request_description = '' for key in sorted(params): encoded_value = quote(params[key], safe='-_.~') @@ -121,7 +104,7 @@ def __init__(self, data, header): self.original = data self.response = None if 'content-md5' in header: - hash_ = calc_md5(self.original) + hash_ = utils.calc_md5(self.original) if header['content-md5'].encode() != hash_: raise MWSError("Wrong Contentlength, maybe amazon error...") @@ -225,7 +208,7 @@ def make_request(self, extra_data, method="GET", **kwargs): description=request_description, signature=quote(signature), ) - headers = {'User-Agent': 'python-amazon-mws/{} (Language=Python)'.format(package_version)} + headers = {'User-Agent': 'python-amazon-mws/{} (Language=Python)'.format(__version__)} headers.update(kwargs.get('extra_headers', {})) try: @@ -318,1029 +301,3 @@ def enumerate_param(self, param, values): "`utils.enumerate_params` for multiple params." ), DeprecationWarning) return utils.enumerate_param(param, values) - - -class Feeds(MWS): - """ - Amazon MWS Feeds API - """ - ACCOUNT_TYPE = "Merchant" - - NEXT_TOKEN_OPERATIONS = [ - 'GetFeedSubmissionList', - ] - - def submit_feed(self, feed, feed_type, marketplaceids=None, - content_type="text/xml", purge='false'): - """ - Uploads a feed ( xml or .tsv ) to the seller's inventory. - Can be used for creating/updating products on Amazon. - """ - data = dict(Action='SubmitFeed', - FeedType=feed_type, - PurgeAndReplace=purge) - data.update(utils.enumerate_param('MarketplaceIdList.Id.', marketplaceids)) - md5_hash = calc_md5(feed) - return self.make_request(data, method="POST", body=feed, - extra_headers={'Content-MD5': md5_hash, 'Content-Type': content_type}) - - @utils.next_token_action('GetFeedSubmissionList') - def get_feed_submission_list(self, feedids=None, max_count=None, feedtypes=None, - processingstatuses=None, fromdate=None, todate=None, - next_token=None): - """ - Returns a list of all feed submissions submitted in the previous 90 days. - That match the query parameters. - """ - - data = dict(Action='GetFeedSubmissionList', - MaxCount=max_count, - SubmittedFromDate=fromdate, - SubmittedToDate=todate,) - data.update(utils.enumerate_param('FeedSubmissionIdList.Id', feedids)) - data.update(utils.enumerate_param('FeedTypeList.Type.', feedtypes)) - data.update(utils.enumerate_param('FeedProcessingStatusList.Status.', processingstatuses)) - return self.make_request(data) - - def get_submission_list_by_next_token(self, token): - """ - Deprecated. - Use `get_feed_submission_list(next_token=token)` instead. - """ - # data = dict(Action='GetFeedSubmissionListByNextToken', NextToken=token) - # return self.make_request(data) - warnings.warn( - "Use `get_feed_submission_list(next_token=token)` instead.", - DeprecationWarning, - ) - return self.get_feed_submission_list(next_token=token) - - def get_feed_submission_count(self, feedtypes=None, processingstatuses=None, fromdate=None, todate=None): - data = dict(Action='GetFeedSubmissionCount', - SubmittedFromDate=fromdate, - SubmittedToDate=todate) - data.update(utils.enumerate_param('FeedTypeList.Type.', feedtypes)) - data.update(utils.enumerate_param('FeedProcessingStatusList.Status.', processingstatuses)) - return self.make_request(data) - - def cancel_feed_submissions(self, feedids=None, feedtypes=None, fromdate=None, todate=None): - data = dict(Action='CancelFeedSubmissions', - SubmittedFromDate=fromdate, - SubmittedToDate=todate) - data.update(utils.enumerate_param('FeedSubmissionIdList.Id.', feedids)) - data.update(utils.enumerate_param('FeedTypeList.Type.', feedtypes)) - return self.make_request(data) - - def get_feed_submission_result(self, feedid): - data = dict(Action='GetFeedSubmissionResult', FeedSubmissionId=feedid) - return self.make_request(data, rootkey='Message') - - -class Reports(MWS): - """ - Amazon MWS Reports API - """ - ACCOUNT_TYPE = "Merchant" - NEXT_TOKEN_OPERATIONS = [ - 'GetReportRequestList', - 'GetReportScheduleList', - ] - - # * REPORTS * # - - def get_report(self, report_id): - data = dict(Action='GetReport', ReportId=report_id) - return self.make_request(data) - - def get_report_count(self, report_types=(), acknowledged=None, fromdate=None, todate=None): - data = dict(Action='GetReportCount', - Acknowledged=acknowledged, - AvailableFromDate=fromdate, - AvailableToDate=todate) - data.update(utils.enumerate_param('ReportTypeList.Type.', report_types)) - return self.make_request(data) - - @utils.next_token_action('GetReportList') - def get_report_list(self, requestids=(), max_count=None, types=(), acknowledged=None, - fromdate=None, todate=None, next_token=None): - data = dict(Action='GetReportList', - Acknowledged=acknowledged, - AvailableFromDate=fromdate, - AvailableToDate=todate, - MaxCount=max_count) - data.update(utils.enumerate_param('ReportRequestIdList.Id.', requestids)) - data.update(utils.enumerate_param('ReportTypeList.Type.', types)) - return self.make_request(data) - - def get_report_list_by_next_token(self, token): - """ - Deprecated. - Use `get_report_list(next_token=token)` instead. - """ - # data = dict(Action='GetReportListByNextToken', NextToken=token) - # return self.make_request(data) - warnings.warn( - "Use `get_report_list(next_token=token)` instead.", - DeprecationWarning, - ) - return self.get_report_list(next_token=token) - - def get_report_request_count(self, report_types=(), processingstatuses=(), - fromdate=None, todate=None): - data = dict(Action='GetReportRequestCount', - RequestedFromDate=fromdate, - RequestedToDate=todate) - data.update(utils.enumerate_param('ReportTypeList.Type.', report_types)) - data.update(utils.enumerate_param('ReportProcessingStatusList.Status.', processingstatuses)) - return self.make_request(data) - - @utils.next_token_action('GetReportRequestList') - def get_report_request_list(self, requestids=(), types=(), processingstatuses=(), - max_count=None, fromdate=None, todate=None, next_token=None): - data = dict(Action='GetReportRequestList', - MaxCount=max_count, - RequestedFromDate=fromdate, - RequestedToDate=todate) - data.update(utils.enumerate_param('ReportRequestIdList.Id.', requestids)) - data.update(utils.enumerate_param('ReportTypeList.Type.', types)) - data.update(utils.enumerate_param('ReportProcessingStatusList.Status.', processingstatuses)) - return self.make_request(data) - - def get_report_request_list_by_next_token(self, token): - """ - Deprecated. - Use `get_report_request_list(next_token=token)` instead. - """ - # data = dict(Action='GetReportRequestListByNextToken', NextToken=token) - # return self.make_request(data) - warnings.warn( - "Use `get_report_request_list(next_token=token)` instead.", - DeprecationWarning, - ) - return self.get_report_request_list(next_token=token) - - def request_report(self, report_type, start_date=None, end_date=None, marketplaceids=()): - data = dict(Action='RequestReport', - ReportType=report_type, - StartDate=start_date, - EndDate=end_date) - data.update(utils.enumerate_param('MarketplaceIdList.Id.', marketplaceids)) - return self.make_request(data) - - # * ReportSchedule * # - - def get_report_schedule_list(self, types=()): - data = dict(Action='GetReportScheduleList') - data.update(utils.enumerate_param('ReportTypeList.Type.', types)) - return self.make_request(data) - - def get_report_schedule_count(self, types=()): - data = dict(Action='GetReportScheduleCount') - data.update(utils.enumerate_param('ReportTypeList.Type.', types)) - return self.make_request(data) - - -class Orders(MWS): - """ - Amazon Orders API - """ - URI = "/Orders/2013-09-01" - VERSION = "2013-09-01" - NAMESPACE = '{https://mws.amazonservices.com/Orders/2013-09-01}' - NEXT_TOKEN_OPERATIONS = [ - 'ListOrders', - 'ListOrderItems', - ] - - @utils.next_token_action('ListOrders') - def list_orders(self, marketplaceids=None, created_after=None, created_before=None, - lastupdatedafter=None, lastupdatedbefore=None, orderstatus=(), - fulfillment_channels=(), payment_methods=(), buyer_email=None, - seller_orderid=None, max_results='100', next_token=None): - - data = dict(Action='ListOrders', - CreatedAfter=created_after, - CreatedBefore=created_before, - LastUpdatedAfter=lastupdatedafter, - LastUpdatedBefore=lastupdatedbefore, - BuyerEmail=buyer_email, - SellerOrderId=seller_orderid, - MaxResultsPerPage=max_results, - ) - data.update(utils.enumerate_param('OrderStatus.Status.', orderstatus)) - data.update(utils.enumerate_param('MarketplaceId.Id.', marketplaceids)) - data.update(utils.enumerate_param('FulfillmentChannel.Channel.', fulfillment_channels)) - data.update(utils.enumerate_param('PaymentMethod.Method.', payment_methods)) - return self.make_request(data) - - def list_orders_by_next_token(self, token): - """ - Deprecated. - Use `list_orders(next_token=token)` instead. - """ - # data = dict(Action='ListOrdersByNextToken', NextToken=token) - # return self.make_request(data) - warnings.warn( - "Use `list_orders(next_token=token)` instead.", - DeprecationWarning, - ) - return self.list_orders(next_token=token) - - def get_order(self, amazon_order_ids): - data = dict(Action='GetOrder') - data.update(utils.enumerate_param('AmazonOrderId.Id.', amazon_order_ids)) - return self.make_request(data) - - @utils.next_token_action('ListOrderItems') - def list_order_items(self, amazon_order_id=None, next_token=None): - data = dict(Action='ListOrderItems', AmazonOrderId=amazon_order_id) - return self.make_request(data) - - def list_order_items_by_next_token(self, token): - """ - Deprecated. - Use `list_order_items(next_token=token)` instead. - """ - # data = dict(Action='ListOrderItemsByNextToken', NextToken=token) - # return self.make_request(data) - warnings.warn( - "Use `list_order_items(next_token=token)` instead.", - DeprecationWarning, - ) - return self.list_order_items(next_token=token) - - -class Products(MWS): - """ - Amazon MWS Products API - """ - URI = '/Products/2011-10-01' - VERSION = '2011-10-01' - NAMESPACE = '{http://mws.amazonservices.com/schema/Products/2011-10-01}' - # NEXT_TOKEN_OPERATIONS = [] - - def list_matching_products(self, marketplaceid, query, contextid=None): - """ - Returns a list of products and their attributes, ordered by - relevancy, based on a search query that you specify. - Your search query can be a phrase that describes the product - or it can be a product identifier such as a UPC, EAN, ISBN, or JAN. - """ - data = dict(Action='ListMatchingProducts', - MarketplaceId=marketplaceid, - Query=query, - QueryContextId=contextid) - return self.make_request(data) - - def get_matching_product(self, marketplaceid, asins): - """ - Returns a list of products and their attributes, based on a list of - ASIN values that you specify. - """ - data = dict(Action='GetMatchingProduct', MarketplaceId=marketplaceid) - data.update(utils.enumerate_param('ASINList.ASIN.', asins)) - return self.make_request(data) - - def get_matching_product_for_id(self, marketplaceid, type_, ids): - """ - Returns a list of products and their attributes, based on a list of - product identifier values (ASIN, SellerSKU, UPC, EAN, ISBN, GCID and JAN) - The identifier type is case sensitive. - Added in Fourth Release, API version 2011-10-01 - """ - data = dict(Action='GetMatchingProductForId', - MarketplaceId=marketplaceid, - IdType=type_) - - data.update(utils.enumerate_param('IdList.Id.', ids)) - return self.make_request(data) - - def get_competitive_pricing_for_sku(self, marketplaceid, skus): - """ - Returns the current competitive pricing of a product, - based on the SellerSKU and MarketplaceId that you specify. - """ - data = dict(Action='GetCompetitivePricingForSKU', MarketplaceId=marketplaceid) - data.update(utils.enumerate_param('SellerSKUList.SellerSKU.', skus)) - return self.make_request(data) - - def get_competitive_pricing_for_asin(self, marketplaceid, asins): - """ - Returns the current competitive pricing of a product, - based on the ASIN and MarketplaceId that you specify. - """ - data = dict(Action='GetCompetitivePricingForASIN', MarketplaceId=marketplaceid) - data.update(utils.enumerate_param('ASINList.ASIN.', asins)) - return self.make_request(data) - - def get_lowest_offer_listings_for_sku(self, marketplaceid, skus, condition="Any", excludeme="False"): - data = dict(Action='GetLowestOfferListingsForSKU', - MarketplaceId=marketplaceid, - ItemCondition=condition, - ExcludeMe=excludeme) - data.update(utils.enumerate_param('SellerSKUList.SellerSKU.', skus)) - return self.make_request(data) - - def get_lowest_offer_listings_for_asin(self, marketplaceid, asins, condition="Any", excludeme="False"): - data = dict(Action='GetLowestOfferListingsForASIN', - MarketplaceId=marketplaceid, - ItemCondition=condition, - ExcludeMe=excludeme) - data.update(utils.enumerate_param('ASINList.ASIN.', asins)) - return self.make_request(data) - - def get_lowest_priced_offers_for_sku(self, marketplaceid, sku, condition="New", excludeme="False"): - data = dict(Action='GetLowestPricedOffersForSKU', - MarketplaceId=marketplaceid, - SellerSKU=sku, - ItemCondition=condition, - ExcludeMe=excludeme) - return self.make_request(data) - - def get_lowest_priced_offers_for_asin(self, marketplaceid, asin, condition="New", excludeme="False"): - data = dict(Action='GetLowestPricedOffersForASIN', - MarketplaceId=marketplaceid, - ASIN=asin, - ItemCondition=condition, - ExcludeMe=excludeme) - return self.make_request(data) - - def get_product_categories_for_sku(self, marketplaceid, sku): - data = dict(Action='GetProductCategoriesForSKU', - MarketplaceId=marketplaceid, - SellerSKU=sku) - return self.make_request(data) - - def get_product_categories_for_asin(self, marketplaceid, asin): - data = dict(Action='GetProductCategoriesForASIN', - MarketplaceId=marketplaceid, - ASIN=asin) - return self.make_request(data) - - def get_my_price_for_sku(self, marketplaceid, skus, condition=None): - data = dict(Action='GetMyPriceForSKU', - MarketplaceId=marketplaceid, - ItemCondition=condition) - data.update(utils.enumerate_param('SellerSKUList.SellerSKU.', skus)) - return self.make_request(data) - - def get_my_price_for_asin(self, marketplaceid, asins, condition=None): - data = dict(Action='GetMyPriceForASIN', - MarketplaceId=marketplaceid, - ItemCondition=condition) - data.update(utils.enumerate_param('ASINList.ASIN.', asins)) - return self.make_request(data) - - -class Sellers(MWS): - """ - Amazon MWS Sellers API - """ - URI = '/Sellers/2011-07-01' - VERSION = '2011-07-01' - NAMESPACE = '{http://mws.amazonservices.com/schema/Sellers/2011-07-01}' - NEXT_TOKEN_OPERATIONS = [ - 'ListMarketplaceParticipations', - ] - - @utils.next_token_action('ListMarketplaceParticipations') - def list_marketplace_participations(self, next_token=None): - """ - Returns a list of marketplaces a seller can participate in and - a list of participations that include seller-specific information in that marketplace. - The operation returns only those marketplaces where the seller's account is - in an active state. - - Run with `next_token` kwarg to call related "ByNextToken" action. - """ - data = dict(Action='ListMarketplaceParticipations') - return self.make_request(data) - - def list_marketplace_participations_by_next_token(self, token): - """ - Deprecated. - Use `list_marketplace_participations(next_token=token)` instead. - """ - # data = dict(Action='ListMarketplaceParticipations', NextToken=token) - # return self.make_request(data) - warnings.warn( - "Use `list_marketplace_participations(next_token=token)` instead.", - DeprecationWarning, - ) - return self.list_marketplace_participations(next_token=token) - - -class Finances(MWS): - """ - Amazon MWS Finances API - """ - URI = "/Finances/2015-05-01" - VERSION = "2015-05-01" - NS = '{https://mws.amazonservices.com/Finances/2015-05-01}' - NEXT_TOKEN_OPERATIONS = [ - 'ListFinancialEventGroups', - 'ListFinancialEvents', - ] - - @utils.next_token_action('ListFinancialEventGroups') - def list_financial_event_groups(self, created_after=None, created_before=None, max_results=None, next_token=None): - """ - Returns a list of financial event groups - """ - data = dict(Action='ListFinancialEventGroups', - FinancialEventGroupStartedAfter=created_after, - FinancialEventGroupStartedBefore=created_before, - MaxResultsPerPage=max_results, - ) - return self.make_request(data) - - def list_financial_event_groups_by_next_token(self, token): - """ - Deprecated. - Use `list_financial_event_groups(next_token=token)` instead. - """ - warnings.warn( - "Use `list_financial_event_groups(next_token=token)` instead.", - DeprecationWarning, - ) - return self.list_financial_event_groups(next_token=token) - - @utils.next_token_action('ListFinancialEvents') - def list_financial_events(self, financial_event_group_id=None, amazon_order_id=None, posted_after=None, - posted_before=None, max_results=None, next_token=None): - """ - Returns financial events for a user-provided FinancialEventGroupId or AmazonOrderId - """ - data = dict(Action='ListFinancialEvents', - FinancialEventGroupId=financial_event_group_id, - AmazonOrderId=amazon_order_id, - PostedAfter=posted_after, - PostedBefore=posted_before, - MaxResultsPerPage=max_results, - ) - return self.make_request(data) - - def list_financial_events_by_next_token(self, token): - """ - Deprecated. - Use `list_financial_events(next_token=token)` instead. - """ - warnings.warn( - "Use `list_financial_events(next_token=token)` instead.", - DeprecationWarning, - ) - return self.list_financial_events(next_token=token) - - -# * Fulfillment APIs * # - - -class InboundShipments(MWS): - """ - Amazon MWS FulfillmentInboundShipment API - """ - URI = "/FulfillmentInboundShipment/2010-10-01" - VERSION = '2010-10-01' - NAMESPACE = '{http://mws.amazonaws.com/FulfillmentInboundShipment/2010-10-01/}' - NEXT_TOKEN_OPERATIONS = [ - 'ListInboundShipments', - 'ListInboundShipmentItems', - ] - SHIPMENT_STATUSES = ['WORKING', 'SHIPPED', 'CANCELLED'] - DEFAULT_SHIP_STATUS = 'WORKING' - LABEL_PREFERENCES = ['SELLER_LABEL', - 'AMAZON_LABEL_ONLY', - 'AMAZON_LABEL_PREFERRED'] - - def __init__(self, *args, **kwargs): - """ - Allow the addition of a from_address dict during object initialization. - kwarg "from_address" is caught and popped here, - then calls set_ship_from_address. - If empty or left out, empty dict is set by default. - """ - self.from_address = {} - addr = kwargs.pop('from_address', None) - if addr is not None: - self.from_address = self.set_ship_from_address(addr) - super(InboundShipments, self).__init__(*args, **kwargs) - - def set_ship_from_address(self, address): - """ - Verifies the structure of an address dictionary. - Once verified against the KEY_CONFIG, saves a parsed version - of that dictionary, ready to send to requests. - """ - # Clear existing - self.from_address = None - - if not address: - raise MWSError('Missing required `address` dict.') - if not isinstance(address, dict): - raise MWSError("`address` must be a dict") - - key_config = [ - # Tuples composed of: - # (input_key, output_key, is_required, default_value) - ('name', 'Name', True, None), - ('address_1', 'AddressLine1', True, None), - ('address_2', 'AddressLine2', False, None), - ('city', 'City', True, None), - ('district_or_county', 'DistrictOrCounty', False, None), - ('state_or_province', 'StateOrProvinceCode', False, None), - ('postal_code', 'PostalCode', False, None), - ('country', 'CountryCode', False, 'US'), - ] - - # Check if all REQUIRED keys in address exist: - if not all(k in address for k in - [c[0] for c in key_config if c[2]]): - # Required parts of address missing - raise MWSError(( - "`address` dict missing required keys: {required}." - "\n- Optional keys: {optional}." - ).format( - required=", ".join([c[0] for c in key_config if c[2]]), - optional=", ".join([c[0] for c in key_config if not c[2]]), - )) - - # Passed tests. Assign values - addr = {'ShipFromAddress.{}'.format(c[1]): address.get(c[0], c[3]) - for c in key_config} - self.from_address = addr - - def parse_item_args(self, item_args, operation): - """ - Parses item arguments sent to create_inbound_shipment_plan, create_inbound_shipment, - and update_inbound_shipment methods. - - `item_args` is expected as an iterable containing dicts. - Each dict should have the following keys: - For `create_inbound_shipment_plan`: - REQUIRED: 'sku', 'quantity' - OPTIONAL: 'quantity_in_case', 'asin', 'condition' - Other operations: - REQUIRED: 'sku', 'quantity' - OPTIONAL: 'quantity_in_case' - If a required key is missing, throws MWSError. - All extra keys are ignored. - - Keys (above) are converted to the appropriate MWS key according to `key_config` (below) - based on the particular operation required. - """ - if not item_args: - raise MWSError("One or more `item` dict arguments required.") - - if operation == 'CreateInboundShipmentPlan': - # `key_config` composed of list of tuples, each tuple compose of: - # (input_key, output_key, is_required, default_value) - key_config = [ - ('sku', 'SellerSKU', True, None), - ('quantity', 'Quantity', True, None), - ('quantity_in_case', 'QuantityInCase', False, None), - ('asin', 'ASIN', False, None), - ('condition', 'Condition', False, None), - ] - # The expected MWS key for quantity is different for this operation. - # This ensures we use the right key later on. - quantity_key = 'Quantity' - else: - key_config = [ - ('sku', 'SellerSKU', True, None), - ('quantity', 'QuantityShipped', True, None), - ('quantity_in_case', 'QuantityInCase', False, None), - ] - quantity_key = 'QuantityShipped' - - items = [] - for item in item_args: - if not isinstance(item, dict): - raise MWSError("`item` argument must be a dict.") - if not all(k in item for k in - [c[0] for c in key_config if c[2]]): - # Required keys of an item line missing - raise MWSError(( - "`item` dict missing required keys: {required}." - "\n- Optional keys: {optional}." - ).format( - required=', '.join([c[0] for c in key_config if c[2]]), - optional=', '.join([c[0] for c in key_config if not c[2]]), - )) - - # Get data from the item. - # Convert to str if present, or leave as None if missing - quantity = item.get('quantity') - if quantity is not None: - quantity = str(quantity) - - quantity_in_case = item.get('quantity_in_case') - if quantity_in_case is not None: - quantity_in_case = str(quantity_in_case) - - item_dict = { - 'SellerSKU': item.get('sku'), - quantity_key: quantity, - 'QuantityInCase': quantity_in_case, - } - item_dict.update({ - c[1]: item.get(c[0], c[3]) - for c in key_config - if c[0] not in ['sku', 'quantity', 'quantity_in_case'] - }) - items.append(item_dict) - - return items - - def create_inbound_shipment_plan(self, items, country_code='US', - subdivision_code='', label_preference=''): - """ - Returns one or more inbound shipment plans, which provide the - information you need to create inbound shipments. - - At least one dictionary must be passed as `args`. Each dictionary - should contain the following keys: - REQUIRED: 'sku', 'quantity' - OPTIONAL: 'asin', 'condition', 'quantity_in_case' - - 'from_address' is required. Call 'set_ship_from_address' first before - using this operation. - """ - if not items: - raise MWSError("One or more `item` dict arguments required.") - subdivision_code = subdivision_code or None - label_preference = label_preference or None - - items = self.parse_item_args(items, 'CreateInboundShipmentPlan') - if not self.from_address: - raise MWSError(( - "ShipFromAddress has not been set. " - "Please use `.set_ship_from_address()` first." - )) - - data = dict( - Action='CreateInboundShipmentPlan', - ShipToCountryCode=country_code, - ShipToCountrySubdivisionCode=subdivision_code, - LabelPrepPreference=label_preference, - ) - data.update(self.from_address) - data.update(utils.enumerate_keyed_param( - 'InboundShipmentPlanRequestItems.member', items, - )) - return self.make_request(data, method="POST") - - def create_inbound_shipment(self, shipment_id, shipment_name, - destination, items, shipment_status='', - label_preference='', case_required=False, - box_contents_source=None): - """ - Creates an inbound shipment to Amazon's fulfillment network. - - At least one dictionary must be passed as `items`. Each dictionary - should contain the following keys: - REQUIRED: 'sku', 'quantity' - OPTIONAL: 'quantity_in_case' - - 'from_address' is required. Call 'set_ship_from_address' first before - using this operation. - """ - assert isinstance(shipment_id, str), "`shipment_id` must be a string." - assert isinstance(shipment_name, str), "`shipment_name` must be a string." - assert isinstance(destination, str), "`destination` must be a string." - - if not items: - raise MWSError("One or more `item` dict arguments required.") - - items = self.parse_item_args(items, 'CreateInboundShipment') - - if not self.from_address: - raise MWSError(( - "ShipFromAddress has not been set. " - "Please use `.set_ship_from_address()` first." - )) - from_address = self.from_address - from_address = {'InboundShipmentHeader.{}'.format(k): v - for k, v in from_address.items()} - - if shipment_status not in self.SHIPMENT_STATUSES: - # Status is required for `create` request. - # Set it to default. - shipment_status = self.DEFAULT_SHIP_STATUS - - if label_preference not in self.LABEL_PREFERENCES: - # Label preference not required. Set to None - label_preference = None - - # Explict True/False for case_required, - # written as the strings MWS expects. - case_required = 'true' if case_required else 'false' - - data = { - 'Action': 'CreateInboundShipment', - 'ShipmentId': shipment_id, - 'InboundShipmentHeader.ShipmentName': shipment_name, - 'InboundShipmentHeader.DestinationFulfillmentCenterId': destination, - 'InboundShipmentHeader.LabelPrepPreference': label_preference, - 'InboundShipmentHeader.AreCasesRequired': case_required, - 'InboundShipmentHeader.ShipmentStatus': shipment_status, - 'InboundShipmentHeader.IntendedBoxContentsSource': box_contents_source, - } - data.update(from_address) - data.update(utils.enumerate_keyed_param( - 'InboundShipmentItems.member', items, - )) - return self.make_request(data, method="POST") - - def update_inbound_shipment(self, shipment_id, shipment_name, - destination, items=None, shipment_status='', - label_preference='', case_required=False, - box_contents_source=None): - """ - Updates an existing inbound shipment in Amazon FBA. - 'from_address' is required. Call 'set_ship_from_address' first before - using this operation. - """ - # Assert these are strings, error out if not. - assert isinstance(shipment_id, str), "`shipment_id` must be a string." - assert isinstance(shipment_name, str), "`shipment_name` must be a string." - assert isinstance(destination, str), "`destination` must be a string." - - # Parse item args - if items: - items = self.parse_item_args(items, 'UpdateInboundShipment') - else: - items = None - - # Raise exception if no from_address has been set prior to calling - if not self.from_address: - raise MWSError(( - "ShipFromAddress has not been set. " - "Please use `.set_ship_from_address()` first." - )) - # Assemble the from_address using operation-specific header - from_address = self.from_address - from_address = {'InboundShipmentHeader.{}'.format(k): v - for k, v in from_address.items()} - - if shipment_status not in self.SHIPMENT_STATUSES: - # Passed shipment status is an invalid choice. - # Remove it from this request by setting it to None. - shipment_status = None - - if label_preference not in self.LABEL_PREFERENCES: - # Passed label preference is an invalid choice. - # Remove it from this request by setting it to None. - label_preference = None - - case_required = 'true' if case_required else 'false' - - data = { - 'Action': 'UpdateInboundShipment', - 'ShipmentId': shipment_id, - 'InboundShipmentHeader.ShipmentName': shipment_name, - 'InboundShipmentHeader.DestinationFulfillmentCenterId': destination, - 'InboundShipmentHeader.LabelPrepPreference': label_preference, - 'InboundShipmentHeader.AreCasesRequired': case_required, - 'InboundShipmentHeader.ShipmentStatus': shipment_status, - 'InboundShipmentHeader.IntendedBoxContentsSource': box_contents_source, - } - data.update(from_address) - if items: - # Update with an items paramater only if they exist. - data.update(utils.enumerate_keyed_param( - 'InboundShipmentItems.member', items, - )) - return self.make_request(data, method="POST") - - def get_prep_instructions_for_sku(self, skus=None, country_code=None): - """ - Returns labeling requirements and item preparation instructions - to help you prepare items for an inbound shipment. - """ - country_code = country_code or 'US' - skus = skus or [] - - # 'skus' should be a unique list, or there may be an error returned. - skus = utils.unique_list_order_preserved(skus) - - data = dict( - Action='GetPrepInstructionsForSKU', - ShipToCountryCode=country_code, - ) - data.update(utils.enumerate_params({ - 'SellerSKUList.ID.': skus, - })) - return self.make_request(data, method="POST") - - def get_prep_instructions_for_asin(self, asins=None, country_code=None): - """ - Returns item preparation instructions to help with - item sourcing decisions. - """ - country_code = country_code or 'US' - asins = asins or [] - - # 'asins' should be a unique list, or there may be an error returned. - asins = utils.unique_list_order_preserved(asins) - - data = dict( - Action='GetPrepInstructionsForASIN', - ShipToCountryCode=country_code, - ) - data.update(utils.enumerate_params({ - 'ASINList.ID.': asins, - })) - return self.make_request(data, method="POST") - - def get_package_labels(self, shipment_id, num_packages, page_type=None): - """ - Returns PDF document data for printing package labels for - an inbound shipment. - """ - data = dict( - Action='GetPackageLabels', - ShipmentId=shipment_id, - PageType=page_type, - NumberOfPackages=str(num_packages), - ) - return self.make_request(data, method="POST") - - def get_transport_content(self, shipment_id): - """ - Returns current transportation information about an - inbound shipment. - """ - data = dict( - Action='GetTransportContent', - ShipmentId=shipment_id - ) - return self.make_request(data, method="POST") - - def estimate_transport_request(self, shipment_id): - """ - Requests an estimate of the shipping cost for an inbound shipment. - """ - data = dict( - Action='EstimateTransportRequest', - ShipmentId=shipment_id, - ) - return self.make_request(data, method="POST") - - def void_transport_request(self, shipment_id): - """ - Voids a previously-confirmed request to ship your inbound shipment - using an Amazon-partnered carrier. - """ - data = dict( - Action='VoidTransportRequest', - ShipmentId=shipment_id - ) - return self.make_request(data, method="POST") - - def get_bill_of_lading(self, shipment_id): - """ - Returns PDF document data for printing a bill of lading - for an inbound shipment. - """ - data = dict( - Action='GetBillOfLading', - ShipmentId=shipment_id, - ) - return self.make_request(data, "POST") - - @utils.next_token_action('ListInboundShipments') - def list_inbound_shipments(self, shipment_ids=None, shipment_statuses=None, - last_updated_after=None, last_updated_before=None,): - """ - Returns list of shipments based on statuses, IDs, and/or - before/after datetimes. - """ - last_updated_after = utils.dt_iso_or_none(last_updated_after) - last_updated_before = utils.dt_iso_or_none(last_updated_before) - - data = dict( - Action='ListInboundShipments', - LastUpdatedAfter=last_updated_after, - LastUpdatedBefore=last_updated_before, - ) - data.update(utils.enumerate_params({ - 'ShipmentStatusList.member.': shipment_statuses, - 'ShipmentIdList.member.': shipment_ids, - })) - return self.make_request(data, method="POST") - - @utils.next_token_action('ListInboundShipmentItems') - def list_inbound_shipment_items(self, shipment_id=None, last_updated_after=None, - last_updated_before=None,): - """ - Returns list of items within inbound shipments and/or - before/after datetimes. - """ - last_updated_after = utils.dt_iso_or_none(last_updated_after) - last_updated_before = utils.dt_iso_or_none(last_updated_before) - - data = dict( - Action='ListInboundShipmentItems', - ShipmentId=shipment_id, - LastUpdatedAfter=last_updated_after, - LastUpdatedBefore=last_updated_before, - ) - return self.make_request(data, method="POST") - - -class Inventory(MWS): - """ - Amazon MWS Inventory Fulfillment API - """ - - URI = '/FulfillmentInventory/2010-10-01' - VERSION = '2010-10-01' - NAMESPACE = "{http://mws.amazonaws.com/FulfillmentInventory/2010-10-01}" - NEXT_TOKEN_OPERATIONS = [ - 'ListInventorySupply', - ] - - @utils.next_token_action('ListInventorySupply') - def list_inventory_supply(self, skus=(), datetime_=None, - response_group='Basic', next_token=None): - """ - Returns information on available inventory - """ - - data = dict(Action='ListInventorySupply', - QueryStartDateTime=datetime_, - ResponseGroup=response_group, - ) - data.update(utils.enumerate_param('SellerSkus.member.', skus)) - return self.make_request(data, "POST") - - def list_inventory_supply_by_next_token(self, token): - """ - Deprecated. - Use `list_inventory_supply(next_token=token)` instead. - """ - # data = dict(Action='ListInventorySupplyByNextToken', NextToken=token) - # return self.make_request(data, "POST") - warnings.warn( - "Use `list_inventory_supply(next_token=token)` instead.", - DeprecationWarning, - ) - return self.list_inventory_supply(next_token=token) - - -class OutboundShipments(MWS): - """ - Amazon MWS Fulfillment Outbound Shipments API - """ - URI = "/FulfillmentOutboundShipment/2010-10-01" - VERSION = "2010-10-01" - NEXT_TOKEN_OPERATIONS = [ - 'ListAllFulfillmentOrders', - ] - # TODO: Complete this class section - - -class Recommendations(MWS): - """ - Amazon MWS Recommendations API - """ - URI = '/Recommendations/2013-04-01' - VERSION = '2013-04-01' - NAMESPACE = "{https://mws.amazonservices.com/Recommendations/2013-04-01}" - NEXT_TOKEN_OPERATIONS = [ - "ListRecommendations", - ] - - def get_last_updated_time_for_recommendations(self, marketplaceid): - """ - Checks whether there are active recommendations for each category for the given marketplace, and if there are, - returns the time when recommendations were last updated for each category. - """ - data = dict(Action='GetLastUpdatedTimeForRecommendations', - MarketplaceId=marketplaceid) - return self.make_request(data, "POST") - - @utils.next_token_action('ListRecommendations') - def list_recommendations(self, marketplaceid=None, - recommendationcategory=None, next_token=None): - """ - Returns your active recommendations for a specific category or for all categories for a specific marketplace. - """ - data = dict(Action="ListRecommendations", - MarketplaceId=marketplaceid, - RecommendationCategory=recommendationcategory) - return self.make_request(data, "POST") - - def list_recommendations_by_next_token(self, token): - """ - Deprecated. - Use `list_recommendations(next_token=token)` instead. - """ - # data = dict(Action="ListRecommendationsByNextToken", - # NextToken=token) - # return self.make_request(data, "POST") - warnings.warn( - "Use `list_recommendations(next_token=token)` instead.", - DeprecationWarning, - ) - return self.list_recommendations(next_token=token) diff --git a/mws/utils.py b/mws/utils.py index b6ea569e..d44b570a 100644 --- a/mws/utils.py +++ b/mws/utils.py @@ -7,9 +7,10 @@ @author: pierre """ from __future__ import absolute_import -from functools import wraps import re +import base64 import datetime +import hashlib import xml.etree.ElementTree as ET @@ -53,7 +54,6 @@ def getvalue(self, item, value=None): class XML2Dict(object): - def __init__(self): pass @@ -108,6 +108,15 @@ def fromstring(self, str_): return ObjectDict({root_tag: root_tree}) +def calc_md5(string): + """ + Calculates the MD5 encryption for the given string + """ + md5_hash = hashlib.md5() + md5_hash.update(string) + return base64.b64encode(md5_hash.digest()).strip(b'\n') + + def enumerate_param(param, values): """ Builds a dictionary of an enumerated parameter, using the param string and some values. @@ -231,27 +240,6 @@ def dt_iso_or_none(dt_obj): return None -def next_token_action(action_name): - """ - Decorator that designates an action as having a "...ByNextToken" associated request. - Checks for a `next_token` kwargs in the request and, if present, redirects the call - to `action_by_next_token` using the given `action_name`. - - Only the `next_token` kwarg is consumed by the "next" call: - all other args and kwargs are ignored and not required. - """ - def _decorator(request_func): - @wraps(request_func) - def _wrapped_func(self, *args, **kwargs): - next_token = kwargs.pop('next_token', None) - if next_token is not None: - # Token captured: run the "next" action. - return self.action_by_next_token(action_name, next_token) - return request_func(self, *args, **kwargs) - return _wrapped_func - return _decorator - - def get_utc_timestamp(): """ Returns the current UTC timestamp in ISO-8601 format. diff --git a/tests/test_next_token_decorator.py b/tests/test_next_token_decorator.py index 57c84f09..1e8cff54 100644 --- a/tests/test_next_token_decorator.py +++ b/tests/test_next_token_decorator.py @@ -27,7 +27,7 @@ def action_by_next_token(self, action, token): modified_action = "{}ByNextToken".format(action) return modified_action, token - @mws.utils.next_token_action(ACTION) + @mws.decorators.next_token_action(ACTION) def target_request_method(self, next_token=None): """ Toy request method, used as the target for our test. diff --git a/tests/test_utils.py b/tests/test_utils.py index c8747591..6b716da3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,4 +1,5 @@ -from mws.mws import calc_md5, calc_request_description +from mws.mws import calc_request_description +from mws.utils import calc_md5 def test_calc_md5(): From 4d2e832f4473cf672d385356479aca2d148fa1e4 Mon Sep 17 00:00:00 2001 From: Galen Rice Date: Mon, 12 Feb 2018 12:34:17 -0500 Subject: [PATCH 06/10] Version bump for feature --- mws/mws.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mws/mws.py b/mws/mws.py index 3b65e25b..6297f08d 100644 --- a/mws/mws.py +++ b/mws/mws.py @@ -23,7 +23,7 @@ from xml.parsers.expat import ExpatError as XMLError -__version__ = '0.8.0' +__version__ = '1.0.0' # See https://images-na.ssl-images-amazon.com/images/G/01/mwsportal/doc/en_US/bde/MWSDeveloperGuide._V357736853_.pdf # page 8 diff --git a/setup.py b/setup.py index ff258563..e8123008 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name='mws', - version='0.8.0', + version='1.0.0', maintainer="James Hiew", maintainer_email="james@hiew.net", url="http://github.com/jameshiew/mws", From 5be78ce66d66fee68925305d96c89363055c830f Mon Sep 17 00:00:00 2001 From: griceturrble Date: Sat, 10 Mar 2018 13:16:30 -0500 Subject: [PATCH 07/10] Move and incorporate new MerchantFulfillment API --- mws/apis/__init__.py | 2 + mws/apis/merchant_fulfillment.py | 67 ++++++++++++++++++++++++++++++++ mws/mws.py | 63 ------------------------------ 3 files changed, 69 insertions(+), 63 deletions(-) create mode 100644 mws/apis/merchant_fulfillment.py diff --git a/mws/apis/__init__.py b/mws/apis/__init__.py index 5de7b34a..4652f811 100644 --- a/mws/apis/__init__.py +++ b/mws/apis/__init__.py @@ -2,6 +2,7 @@ from .finances import Finances from .inbound_shipments import InboundShipments from .inventory import Inventory +from .merchant_fulfillment import MerchantFulfillment from .offamazonpayments import OffAmazonPayments from .orders import Orders from .products import Products @@ -14,6 +15,7 @@ 'Finances', 'InboundShipments', 'Inventory', + 'MerchantFulfillment', 'OffAmazonPayments', 'Orders', 'Products', diff --git a/mws/apis/merchant_fulfillment.py b/mws/apis/merchant_fulfillment.py new file mode 100644 index 00000000..d942fabc --- /dev/null +++ b/mws/apis/merchant_fulfillment.py @@ -0,0 +1,67 @@ +""" +Amazon MWS Merchant Fulfillment API +""" +from __future__ import absolute_import + +from ..mws import MWS +from .. import utils + + +class MerchantFulfillment(MWS): + """ + Amazon MWS Merchant Fulfillment API + """ + URI = "/MerchantFulfillment/2015-06-01" + VERSION = "2015-06-01" + NS = '{https://mws.amazonservices.com/MerchantFulfillment/2015-06-01}' + + def get_eligible_shipping_services(self, amazon_order_id=None, seller_orderid=None, item_list=[], + ship_from_address={}, package_dimensions={}, weight={}, + must_arrive_by_date=None, ship_date=None, + shipping_service_options={}, label_customization={}): + + data = { + "Action": "GetEligibleShippingServices", + "ShipmentRequestDetails.AmazonOrderId": amazon_order_id, + "ShipmentRequestDetails.SellerOrderId": seller_orderid, + "ShipmentRequestDetails.MustArriveByDate": must_arrive_by_date, + "ShipmentRequestDetails.ShipDate": ship_date + } + data.update(utils.enumerate_keyed_param("ShipmentRequestDetails.ItemList.Item", item_list)) + data.update(utils.dict_keyed_param("ShipmentRequestDetails.ShipFromAddress", ship_from_address)) + data.update(utils.dict_keyed_param("ShipmentRequestDetails.PackageDimensions", package_dimensions)) + data.update(utils.dict_keyed_param("ShipmentRequestDetails.Weight", weight)) + data.update(utils.dict_keyed_param("ShipmentRequestDetails.ShippingServiceOptions", shipping_service_options)) + data.update(utils.dict_keyed_param("ShipmentRequestDetails.LabelCustomization", label_customization)) + return self.make_request(data) + + def create_shipment(self, amazon_order_id=None, seller_orderid=None, item_list=[], ship_from_address={}, + package_dimensions={}, weight={}, must_arrive_by_date=None, ship_date=None, + shipping_service_options={}, label_customization={}, shipping_service_id=None, + shipping_service_offer_id=None, hazmat_type=None): + + data = { + "Action": "CreateShipment", + "ShipmentRequestDetails.AmazonOrderId": amazon_order_id, + "ShipmentRequestDetails.SellerOrderId": seller_orderid, + "ShipmentRequestDetails.MustArriveByDate": must_arrive_by_date, + "ShipmentRequestDetails.ShipDate": ship_date, + "ShippingServiceId": shipping_service_id, + "ShippingServiceOfferId": shipping_service_offer_id, + "HazmatType": hazmat_type + } + data.update(utils.enumerate_keyed_param("ShipmentRequestDetails.ItemList.Item", item_list)) + data.update(utils.dict_keyed_param("ShipmentRequestDetails.ShipFromAddress", ship_from_address)) + data.update(utils.dict_keyed_param("ShipmentRequestDetails.PackageDimensions", package_dimensions)) + data.update(utils.dict_keyed_param("ShipmentRequestDetails.Weight", weight)) + data.update(utils.dict_keyed_param("ShipmentRequestDetails.ShippingServiceOptions", shipping_service_options)) + data.update(utils.dict_keyed_param("ShipmentRequestDetails.LabelCustomization", label_customization)) + return self.make_request(data) + + def get_shipment(self, shipment_id=None): + data = dict(Action="GetShipment", ShipmentId=shipment_id) + return self.make_request(data) + + def cancel_shipment(self, shipment_id=None): + data = dict(Action="CancelShipment", ShipmentId=shipment_id) + return self.make_request(data) diff --git a/mws/mws.py b/mws/mws.py index df90ce9d..6297f08d 100644 --- a/mws/mws.py +++ b/mws/mws.py @@ -24,7 +24,6 @@ __version__ = '1.0.0' - 'MerchantFulfillment', # See https://images-na.ssl-images-amazon.com/images/G/01/mwsportal/doc/en_US/bde/MWSDeveloperGuide._V357736853_.pdf # page 8 @@ -302,65 +301,3 @@ def enumerate_param(self, param, values): "`utils.enumerate_params` for multiple params." ), DeprecationWarning) return utils.enumerate_param(param, values) - - - -# * Merchant Fulfillment API * # -class MerchantFulfillment(MWS): - """ - Amazon MWS Merchant Fulfillment API - """ - URI = "/MerchantFulfillment/2015-06-01" - VERSION = "2015-06-01" - NS = '{https://mws.amazonservices.com/MerchantFulfillment/2015-06-01}' - - def get_eligible_shipping_services(self, amazon_order_id=None, seller_orderid=None, item_list=[], - ship_from_address={}, package_dimensions={}, weight={}, - must_arrive_by_date=None, ship_date=None, - shipping_service_options={}, label_customization={}): - - data = { - "Action": "GetEligibleShippingServices", - "ShipmentRequestDetails.AmazonOrderId": amazon_order_id, - "ShipmentRequestDetails.SellerOrderId": seller_orderid, - "ShipmentRequestDetails.MustArriveByDate": must_arrive_by_date, - "ShipmentRequestDetails.ShipDate": ship_date - } - data.update(utils.enumerate_keyed_param("ShipmentRequestDetails.ItemList.Item", item_list)) - data.update(utils.dict_keyed_param("ShipmentRequestDetails.ShipFromAddress", ship_from_address)) - data.update(utils.dict_keyed_param("ShipmentRequestDetails.PackageDimensions", package_dimensions)) - data.update(utils.dict_keyed_param("ShipmentRequestDetails.Weight", weight)) - data.update(utils.dict_keyed_param("ShipmentRequestDetails.ShippingServiceOptions", shipping_service_options)) - data.update(utils.dict_keyed_param("ShipmentRequestDetails.LabelCustomization", label_customization)) - return self.make_request(data) - - def create_shipment(self, amazon_order_id=None, seller_orderid=None, item_list=[], ship_from_address={}, - package_dimensions={}, weight={}, must_arrive_by_date=None, ship_date=None, - shipping_service_options={}, label_customization={}, shipping_service_id=None, - shipping_service_offer_id=None, hazmat_type=None): - - data = { - "Action": "CreateShipment", - "ShipmentRequestDetails.AmazonOrderId": amazon_order_id, - "ShipmentRequestDetails.SellerOrderId": seller_orderid, - "ShipmentRequestDetails.MustArriveByDate": must_arrive_by_date, - "ShipmentRequestDetails.ShipDate": ship_date, - "ShippingServiceId": shipping_service_id, - "ShippingServiceOfferId": shipping_service_offer_id, - "HazmatType": hazmat_type - } - data.update(utils.enumerate_keyed_param("ShipmentRequestDetails.ItemList.Item", item_list)) - data.update(utils.dict_keyed_param("ShipmentRequestDetails.ShipFromAddress", ship_from_address)) - data.update(utils.dict_keyed_param("ShipmentRequestDetails.PackageDimensions", package_dimensions)) - data.update(utils.dict_keyed_param("ShipmentRequestDetails.Weight", weight)) - data.update(utils.dict_keyed_param("ShipmentRequestDetails.ShippingServiceOptions", shipping_service_options)) - data.update(utils.dict_keyed_param("ShipmentRequestDetails.LabelCustomization", label_customization)) - return self.make_request(data) - - def get_shipment(self, shipment_id=None): - data = dict(Action="GetShipment", ShipmentId=shipment_id) - return self.make_request(data) - - def cancel_shipment(self, shipment_id=None): - data = dict(Action="CancelShipment", ShipmentId=shipment_id) - return self.make_request(data) \ No newline at end of file From ac2e0426f7df2867048987d86bd609e0d000657f Mon Sep 17 00:00:00 2001 From: griceturrble Date: Sat, 10 Mar 2018 14:15:54 -0500 Subject: [PATCH 08/10] merge artifacts removed [broken] --- mws/mws.py | 178 ----------------------------------------------------- 1 file changed, 178 deletions(-) diff --git a/mws/mws.py b/mws/mws.py index caffd40a..03e47783 100644 --- a/mws/mws.py +++ b/mws/mws.py @@ -314,181 +314,3 @@ def enumerate_param(self, param, values): "`utils.enumerate_params` for multiple params." ), DeprecationWarning) return utils.enumerate_param(param, values) - - def get_inbound_guidance_for_sku(self, sku_list, marketplace_id): - if not isinstance(sku_list, (list, tuple, set)): - sku_list = [sku_list] - data = dict( - Action='GetInboundGuidanceForSKU', - MarketplaceId=marketplace_id, - ) - data.update(utils.enumerate_param('SellerSKUList.Id', sku_list)) - return self.make_request(data) - - def get_inbound_guidance_for_asin(self, asin_list, marketplace_id): - if not isinstance(asin_list, (list, tuple, set)): - asin_list = [asin_list] - data = dict( - Action='GetInboundGuidanceForASIN', - MarketplaceId=marketplace_id, - ) - data.update(utils.enumerate_param('ASINList.Id', asin_list)) - return self.make_request(data) - - def get_pallet_labels(self, shipment_id, page_type, num_pallets): - """ - Returns pallet labels. - `shipment_id` must match a valid, current shipment. - `page_type` expected to be string matching one of following (not checked, in case Amazon requirements change): - PackageLabel_Letter_2 - PackageLabel_Letter_6 - PackageLabel_A4_2 - PackageLabel_A4_4 - PackageLabel_Plain_Pape - `num_pallets` is integer, number of labels to create. - - Documentation: - http://docs.developer.amazonservices.com/en_US/fba_inbound/FBAInbound_GetPalletLabels.html - """ - data = dict( - Action='GetPalletLabels', - ShipmentId=shipment_id, - PageType=page_type, - NumberOfPallets=num_pallets, - ) - return self.make_request(data) - - def get_unique_package_labels(self, shipment_id, page_type, package_ids): - """ - Returns unique package labels for faster and more accurate shipment processing at the Amazon fulfillment center. - - `shipment_id` must match a valid, current shipment. - `page_type` expected to be string matching one of following (not checked, in case Amazon requirements change): - PackageLabel_Letter_2 - PackageLabel_Letter_6 - PackageLabel_A4_2 - PackageLabel_A4_4 - PackageLabel_Plain_Pape - `package_ids` a single package identifier, or a list/tuple/set of identifiers, specifying for which package(s) - you want package labels printed. - - Documentation: - http://docs.developer.amazonservices.com/en_US/fba_inbound/FBAInbound_GetUniquePackageLabels.html - """ - data = dict( - Action='GetUniquePackageLabels', - ShipmentId=shipment_id, - PageType=page_type, - ) - if not isinstance(package_ids, (list, tuple, set)): - package_ids = [package_ids] - data.update(utils.enumerate_param('PackageLabelsToPrint.member.', package_ids)) - return self.make_request(data) - - def confirm_transport_request(self, shipment_id): - """ - Confirms that you accept the Amazon-partnered shipping estimate and you request that the - Amazon-partnered carrier ship your inbound shipment. - - Documentation: - http://docs.developer.amazonservices.com/en_US/fba_inbound/FBAInbound_ConfirmTransportRequest.html - """ - data = dict( - Action='ConfirmTransportRequest', - ShipmentId=shipment_id, - ) - return self.make_request(data) - - # TODO this method is incomplete: it should be able to account for all TransportDetailInput types - # docs: http://docs.developer.amazonservices.com/en_US/fba_inbound/FBAInbound_Datatypes.html#TransportDetailInput - # def put_transport_content(self, shipment_id, is_partnered, shipment_type, carrier_name, tracking_id): - # data = dict( - # Action='PutTransportContent', - # ShipmentId=shipment_id, - # IsPartnered=is_partnered, - # ShipmentType=shipment_type, - # ) - # data['TransportDetails.NonPartneredSmallParcelData.CarrierName'] = carrier_name - # if isinstance(tracking_id, tuple): - # count = 0 - # for track in tracking_id: - # data[ - # 'TransportDetails.NonPartneredSmallParcelData.PackageList.member.{}.TrackingId'.format(count + 1) - # ] = track - # return self.make_request(data) - - def confirm_preorder(self, shipment_id, need_by_date): - data = dict( - Action='ConfirmPreorder', - ShipmentId=shipment_id, - NeedByDate=need_by_date, - ) - return self.make_request(data) - - def get_preorder_info(self, shipment_id): - data = dict( - Action='GetPreorderInfo', - ShipmentId=shipment_id, - ) - return self.make_request(data) - - - """ - http://docs.developer.amazonservices.com/en_UK/merch_fulfill/MerchFulfill_GetEligibleShippingServices.html - - :param amazon_order_id: Required - :param seller_orderid: - :param item_list: Required - :param ship_from_address: Required - :param package_dimensions: Required - :param weight: Required - :param must_arrive_by_date: - :param ship_date: - :param shipping_service_options: Required - :param label_customization: - :return: - """ - - if ship_from_address is None: - ship_from_address = {} - if package_dimensions is None: - package_dimensions = {} - if weight is None: - weight = {} - if item_list is None: - item_list = [] - if shipping_service_options is None: - shipping_service_options = {} - if label_customization is None: - label_customization = {} - """ - http://docs.developer.amazonservices.com/en_UK/merch_fulfill/MerchFulfill_CreateShipment.html - - :param amazon_order_id: Required - :param seller_orderid: - :param item_list: Required - :param ship_from_address: Required - :param package_dimensions: Required - :param weight: Required - :param must_arrive_by_date: - :param ship_date: - :param shipping_service_options: - :param label_customization: - :param shipping_service_id: Required - :param shipping_service_offer_id: - :param hazmat_type: - :return: - """ - - if item_list is None: - item_list = [] - if ship_from_address is None: - ship_from_address = {} - if package_dimensions is None: - package_dimensions = {} - if weight is None: - weight = {} - if shipping_service_options is None: - shipping_service_options = {} - if label_customization is None: - label_customization = {} \ No newline at end of file From aafe6e5c0b6793a8043ca1423ac5d853683e5008 Mon Sep 17 00:00:00 2001 From: griceturrble Date: Sat, 10 Mar 2018 14:19:03 -0500 Subject: [PATCH 09/10] Methods from PR #22 added. --- mws/apis/inbound_shipments.py | 117 ++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/mws/apis/inbound_shipments.py b/mws/apis/inbound_shipments.py index 8e147a87..a93349ab 100644 --- a/mws/apis/inbound_shipments.py +++ b/mws/apis/inbound_shipments.py @@ -460,3 +460,120 @@ def list_inbound_shipment_items(self, shipment_id=None, last_updated_after=None, LastUpdatedBefore=last_updated_before, ) return self.make_request(data, method="POST") + + def get_inbound_guidance_for_sku(self, sku_list, marketplace_id): + if not isinstance(sku_list, (list, tuple, set)): + sku_list = [sku_list] + data = dict( + Action='GetInboundGuidanceForSKU', + MarketplaceId=marketplace_id, + ) + data.update(utils.enumerate_param('SellerSKUList.Id', sku_list)) + return self.make_request(data) + + def get_inbound_guidance_for_asin(self, asin_list, marketplace_id): + if not isinstance(asin_list, (list, tuple, set)): + asin_list = [asin_list] + data = dict( + Action='GetInboundGuidanceForASIN', + MarketplaceId=marketplace_id, + ) + data.update(utils.enumerate_param('ASINList.Id', asin_list)) + return self.make_request(data) + + def get_pallet_labels(self, shipment_id, page_type, num_pallets): + """ + Returns pallet labels. + `shipment_id` must match a valid, current shipment. + `page_type` expected to be string matching one of following (not checked, in case Amazon requirements change): + PackageLabel_Letter_2 + PackageLabel_Letter_6 + PackageLabel_A4_2 + PackageLabel_A4_4 + PackageLabel_Plain_Pape + `num_pallets` is integer, number of labels to create. + + Documentation: + http://docs.developer.amazonservices.com/en_US/fba_inbound/FBAInbound_GetPalletLabels.html + """ + data = dict( + Action='GetPalletLabels', + ShipmentId=shipment_id, + PageType=page_type, + NumberOfPallets=num_pallets, + ) + return self.make_request(data) + + def get_unique_package_labels(self, shipment_id, page_type, package_ids): + """ + Returns unique package labels for faster and more accurate shipment processing at the Amazon fulfillment center. + + `shipment_id` must match a valid, current shipment. + `page_type` expected to be string matching one of following (not checked, in case Amazon requirements change): + PackageLabel_Letter_2 + PackageLabel_Letter_6 + PackageLabel_A4_2 + PackageLabel_A4_4 + PackageLabel_Plain_Pape + `package_ids` a single package identifier, or a list/tuple/set of identifiers, specifying for which package(s) + you want package labels printed. + + Documentation: + http://docs.developer.amazonservices.com/en_US/fba_inbound/FBAInbound_GetUniquePackageLabels.html + """ + data = dict( + Action='GetUniquePackageLabels', + ShipmentId=shipment_id, + PageType=page_type, + ) + if not isinstance(package_ids, (list, tuple, set)): + package_ids = [package_ids] + data.update(utils.enumerate_param('PackageLabelsToPrint.member.', package_ids)) + return self.make_request(data) + + def confirm_transport_request(self, shipment_id): + """ + Confirms that you accept the Amazon-partnered shipping estimate and you request that the + Amazon-partnered carrier ship your inbound shipment. + + Documentation: + http://docs.developer.amazonservices.com/en_US/fba_inbound/FBAInbound_ConfirmTransportRequest.html + """ + data = dict( + Action='ConfirmTransportRequest', + ShipmentId=shipment_id, + ) + return self.make_request(data) + + # TODO this method is incomplete: it should be able to account for all TransportDetailInput types + # docs: http://docs.developer.amazonservices.com/en_US/fba_inbound/FBAInbound_Datatypes.html#TransportDetailInput + # def put_transport_content(self, shipment_id, is_partnered, shipment_type, carrier_name, tracking_id): + # data = dict( + # Action='PutTransportContent', + # ShipmentId=shipment_id, + # IsPartnered=is_partnered, + # ShipmentType=shipment_type, + # ) + # data['TransportDetails.NonPartneredSmallParcelData.CarrierName'] = carrier_name + # if isinstance(tracking_id, tuple): + # count = 0 + # for track in tracking_id: + # data[ + # 'TransportDetails.NonPartneredSmallParcelData.PackageList.member.{}.TrackingId'.format(count + 1) + # ] = track + # return self.make_request(data) + + def confirm_preorder(self, shipment_id, need_by_date): + data = dict( + Action='ConfirmPreorder', + ShipmentId=shipment_id, + NeedByDate=need_by_date, + ) + return self.make_request(data) + + def get_preorder_info(self, shipment_id): + data = dict( + Action='GetPreorderInfo', + ShipmentId=shipment_id, + ) + return self.make_request(data) From c3c95dea137e0f721f164cf4c77d7ab1af27285a Mon Sep 17 00:00:00 2001 From: griceturrble Date: Sat, 10 Mar 2018 14:19:37 -0500 Subject: [PATCH 10/10] Changes from PR #53 merged --- mws/apis/merchant_fulfillment.py | 72 +++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/mws/apis/merchant_fulfillment.py b/mws/apis/merchant_fulfillment.py index d942fabc..7ddad91d 100644 --- a/mws/apis/merchant_fulfillment.py +++ b/mws/apis/merchant_fulfillment.py @@ -15,10 +15,39 @@ class MerchantFulfillment(MWS): VERSION = "2015-06-01" NS = '{https://mws.amazonservices.com/MerchantFulfillment/2015-06-01}' - def get_eligible_shipping_services(self, amazon_order_id=None, seller_orderid=None, item_list=[], - ship_from_address={}, package_dimensions={}, weight={}, + def get_eligible_shipping_services(self, amazon_order_id=None, seller_orderid=None, item_list=None, + ship_from_address=None, package_dimensions=None, weight=None, must_arrive_by_date=None, ship_date=None, - shipping_service_options={}, label_customization={}): + shipping_service_options=None, label_customization=None): + + """ + http://docs.developer.amazonservices.com/en_UK/merch_fulfill/MerchFulfill_GetEligibleShippingServices.html + + :param amazon_order_id: Required + :param seller_orderid: + :param item_list: Required + :param ship_from_address: Required + :param package_dimensions: Required + :param weight: Required + :param must_arrive_by_date: + :param ship_date: + :param shipping_service_options: Required + :param label_customization: + :return: + """ + + if ship_from_address is None: + ship_from_address = {} + if package_dimensions is None: + package_dimensions = {} + if weight is None: + weight = {} + if item_list is None: + item_list = [] + if shipping_service_options is None: + shipping_service_options = {} + if label_customization is None: + label_customization = {} data = { "Action": "GetEligibleShippingServices", @@ -35,10 +64,41 @@ def get_eligible_shipping_services(self, amazon_order_id=None, seller_orderid=No data.update(utils.dict_keyed_param("ShipmentRequestDetails.LabelCustomization", label_customization)) return self.make_request(data) - def create_shipment(self, amazon_order_id=None, seller_orderid=None, item_list=[], ship_from_address={}, - package_dimensions={}, weight={}, must_arrive_by_date=None, ship_date=None, - shipping_service_options={}, label_customization={}, shipping_service_id=None, + def create_shipment(self, amazon_order_id=None, seller_orderid=None, item_list=None, ship_from_address=None, + package_dimensions=None, weight=None, must_arrive_by_date=None, ship_date=None, + shipping_service_options=None, label_customization=None, shipping_service_id=None, shipping_service_offer_id=None, hazmat_type=None): + """ + http://docs.developer.amazonservices.com/en_UK/merch_fulfill/MerchFulfill_CreateShipment.html + + :param amazon_order_id: Required + :param seller_orderid: + :param item_list: Required + :param ship_from_address: Required + :param package_dimensions: Required + :param weight: Required + :param must_arrive_by_date: + :param ship_date: + :param shipping_service_options: + :param label_customization: + :param shipping_service_id: Required + :param shipping_service_offer_id: + :param hazmat_type: + :return: + """ + + if item_list is None: + item_list = [] + if ship_from_address is None: + ship_from_address = {} + if package_dimensions is None: + package_dimensions = {} + if weight is None: + weight = {} + if shipping_service_options is None: + shipping_service_options = {} + if label_customization is None: + label_customization = {} data = { "Action": "CreateShipment",