Source code for pymws.pymws

"""Main module."""
import base64
import hashlib
import hmac
from builtins import str as text
from datetime import date, datetime

import requests
from lxml import objectify

from .exceptions import MWSError, AccessDenied, QuotaExceeded, RequestThrottled
from .feeds import Feeds
from .orders import Orders
from .products import Products
from .reports import Reports
from .fulfillment.outbound_shipment import OutboundShipment
from .fulfillment.inbound_shipment import InboundShipment
from .utils import get_marketplace, parse_xsv, get_md5_hash

try:
    from urllib.parse import urlparse, quote
except ImportError:
    # py2
    from urlparse import urlparse
    from urllib import quote


MWS_SAFE = '-_.~'.encode('utf-8')


[docs]class MWS(object): """ Primary client class that acts as a gateway to all of the functionality offered by this package. Example usage:: client = MWS( marketplace="US", merchant_id="1234", access_key_id="key", secret_key="secret", auth_token="token" ) client.reports.get_reports_list() :param marketplace: marketplace to connect to. :param merchant_id: Amazon merchant it. :param access_key_id: Access key of your app :param secret_key: Secret key of your app :param auth_token: Token obtained by the merchant after installing your app. """ def __init__( self, marketplace, merchant_id=None, access_key_id=None, secret_key=None, auth_token=None): self.marketplace = get_marketplace(marketplace) self.merchant_id = merchant_id self.access_key_id = access_key_id self.secret_key = secret_key self.auth_token = auth_token self.session = requests.Session() self.user_agent = 'pymws/0.1 (Language=Python)' @property def products(self): """ Fetch the products API client. Returns an instance of :class:`pymws.products.Products` """ return Products(self) @property def orders(self): """ Fetch the order API client Returns an instance of :class:`pymws.orders.Orders` """ return Orders(self) @property def reports(self): """ Fetch the reports API client Returns an instance of :class:`pymws.reports.Reports` """ return Reports(self) @property def feeds(self): """ Fetch the feeds API client Returns an instance of :class:`pymws.feeds.Feeds` """ return Feeds(self) @property def fulfillment_outbound_shipment(self): """ Fetch the Fulfillment outbound shipment API client Returns an instance of :class:`pymws.fulfillment.outbound_shipment.OutboundShipment` """ return OutboundShipment(self) @property def fulfillment_inbound_shipment(self): """ Fetch the Fulfillment inbound shipment API client Returns an instance of :class:`pymws.fulfillment.inbound_shipment.InboundShipment` """ return InboundShipment(self) def get(self, action, uri, req_params, version): return self._request( 'GET', action, uri, req_params, version ) def post(self, action, uri, req_params, version, body=None, content_type=None): return self._request( 'POST', action, uri, req_params, version, body, content_type ) def _request(self, http_verb, action, uri, req_params, version, body=None, content_type=None): """ Build a request, parse the response and handle errors """ if body is not None: req_params['ContentMD5Value'] = get_md5_hash(body) query_string = self.get_query_string(action, req_params, version) signature = self.get_signature(http_verb, uri, query_string) url = "{endpoint}{uri}".format( endpoint=self.marketplace.endpoint, uri=uri, ) headers = { 'User-Agent': self.user_agent, } if content_type: headers['Content-Type'] = content_type response = self.session.request( http_verb, url, data=body, params="{query_string}&Signature={signature}".format( query_string=query_string, signature=quote(signature) ), headers=headers ) if response.status_code == 401: raise AccessDenied(response.text) elif response.status_code == 503: error_code = objectify.fromstring(response.text).Error.Code if error_code == 'RequestThrottled': raise RequestThrottled(response.text) elif error_code == 'QuotaExceeded': raise QuotaExceeded(response.text) elif response.status_code != 200: raise MWSError(response.text) if response.headers['content-type'].startswith('text/xml'): xml = objectify.fromstring(response.content) result_el = '{}Result'.format(action) if hasattr(xml, result_el): return getattr(xml, result_el) return xml elif response.headers['content-type'].startswith('text/plain'): return parse_xsv(response.content.decode(encoding="iso-8859-1")) else: return response.text def get_query_string(self, action, req_params, version): params = { 'AWSAccessKeyId': self.access_key_id, 'Action': action, 'MWSAuthToken': self.auth_token, 'SellerId': self.merchant_id, 'SignatureMethod': 'HmacSHA256', 'SignatureVersion': '2', 'Timestamp': datetime.utcnow().isoformat(), 'Version': version, } params.update(req_params) query_string = self.build_query_string(params) return query_string def get_signature(self, http_verb, uri, query_string): to_sign = [ http_verb, urlparse(self.marketplace.endpoint).netloc, uri, query_string ] return base64.b64encode( hmac.new( self.secret_key.encode('utf-8'), '\n'.join(to_sign).encode('utf-8'), hashlib.sha256, ).digest() ).decode('utf-8') def build_query_string(self, params): """ create the query string to be signed * Sort the UTF-8 query string components by parameter name * URL encode the parameter name and values Because of the following issues, this method cannot directly call urlencode because urlencode does not support quoting the way amazon wants, it always does quote_plus. """ query = [] for key in sorted(params.keys()): value = params[key] if isinstance(value, (date, datetime)): value = value.isoformat() if not isinstance(value, text): value = str(value) query.append( '{}={}'.format( quote(key, safe=MWS_SAFE), quote(value.encode('utf-8'), safe=MWS_SAFE) ) ) return '&'.join(query)