Source code for mws.models.inbound_shipments

"""DataType models for InboundShipments API."""

import datetime
from enum import Enum
from typing import Dict, List, Union

from mws.models.base import MWSDataType
from mws.utils.collections import DotDict
from mws.utils.params import enumerate_keyed_param

__all__ = [

[docs]class Address(MWSDataType): """Postal address information. `MWS docs: Address Datatype <>`_ """ def __init__( self, name: str = None, address_line_1: str = None, address_line_2: str = None, city: str = None, district_or_county: str = None, state_or_province_code: str = None, country_code: str = "US", postal_code: Union[str, int] = None, ): = name self.address_line_1 = address_line_1 self.address_line_2 = address_line_2 = city self.district_or_county = district_or_county self.state_or_province_code = state_or_province_code self.country_code = country_code self.postal_code = postal_code def params_dict(self) -> dict: return { "Name":, "AddressLine1": self.address_line_1, "AddressLine2": self.address_line_2, "City":, "DistrictOrCounty": self.district_or_county, "StateOrProvinceCode": self.state_or_province_code, "CountryCode": self.country_code, "PostalCode": self.postal_code, }
[docs] @classmethod def from_legacy_dict(cls, value: dict) -> "Address": """Create an Address from a legacy structured dict.""" legacy_keys = [ "name", "address_1", "address_2", "city", "district_or_county", "state_or_province", "postal_code", "country", ] conversions = { "address_1": "address_line_1", "address_2": "address_line_2", "state_or_province": "state_or_province_code", "country": "country_code", } addr = {} for key, val in value.items(): if key in legacy_keys: # Convert a key to a new version if present, # or use the old one outkey = conversions.get(key, key) addr[outkey] = val return cls(**addr)
[docs]class PrepInstruction(Enum): """Enumeration of preparation instruction types. `MWS docs: PrepInstruction Datatype <>`_ """ POLYBAGGING = ("Polybagging", "polybagging is required") BUBBLEWRAPPING = ("BubbleWrapping", "bubble wrapping is required") TAPING = ("Taping", "taping is required") BLACKSHRINKWRAPPING = ("BlackShrinkWrapping", "black shrink wrapping is required") LABELING = ("Labeling", "the FNSKU label should be applied to the item") HANGGARMENT = ("HangGarment", "the item should be placed on a hanger") def __init__(self, code, description): self.code = code self.description = description @property def value(self): return self.code
[docs]class PrepDetails(MWSDataType): """A preparation instruction, and who is responsible for that preparation. `MWS docs: PrepDetails Datatype <>`_ ``prep_instruction`` accepts either a string or an instance of the :py:class:`PrepInstruction <mws.InboundShipments.PrepInstruction>` enum, detailing the type of prep to perform. ``prep_owner`` (optional) accepts a string, typically "AMAZON" or "SELLER", to indicate who is responsible for the prep. You can use ``PrepDetails.AMAZON`` or ``PrepDetails.SELLER`` to fill in these values. Defaults to "SELLER". """ AMAZON = "AMAZON" SELLER = "SELLER" def __init__( self, prep_instruction: Union[PrepInstruction, str], prep_owner: str = SELLER, ): self.prep_instruction = prep_instruction self.prep_owner = prep_owner def params_dict(self) -> dict: return { "PrepInstruction": self.prep_instruction, "PrepOwner": self.prep_owner, }
[docs]class ItemCondition(str, Enum): """Condition value for an item included with a CreateInboundShipmentPlan request. Values are defined within the `InboundShipmentPlanRequestItem Datatype documentation <>`_. """ NEW_ITEM = "NewItem" NEW_WITH_WARRANTY = "NewWithWarranty" NEW_OEM = "NewOEM" NEW_OPEN_BOX = "NewOpenBox" USED_LIKE_NEW = "UsedLikeNew" USED_VERY_GOOD = "UsedVeryGood" USED_GOOD = "UsedGood" USED_ACCEPTABLE = "UsedAcceptable" USED_POOR = "UsedPoor" USED_REFURBISHED = "UsedRefurbished" COLLECTIBLE_LIKE_NEW = "CollectibleLikeNew" COLLECTIBLE_VERY_GOOD = "CollectibleVeryGood" COLLECTIBLE_GOOD = "CollectibleGood" COLLECTIBLE_ACCEPTABLE = "CollectibleAcceptable" COLLECTIBLE_POOR = "CollectiblePoor" REFURBISHED_WITH_WARRANTY = "RefurbishedWithWarranty" REFURBISHED = "Refurbished" CLUB = "Club"
class BaseInboundShipmentItem(MWSDataType): """Base class for Item information for creating an shipments and shipment plans. Subclasses of this class may be submitted with a call to the either ``create_inbound_shipment_plan`` or ``create_inbound_shipment``, depending on the nature of that particular subclass. Only ``sku`` and ``quantity`` are required for each item. Include ``quantity_in_case`` if your items are case-packed. ``prep_details_list`` (optional) expects an iterable of :py:class:`PrepDetails <mws.InboundShipments.PrepDetails>` instances. """ quantity_param = "" """The key to use for the ``quantity`` arg, when generating parameters. The different calls use different names for ``quantity`` parameter, so this must be defined in subclasses. """ def __init__( self, sku: str, quantity: int, quantity_in_case: int = None, prep_details_list: List[PrepDetails] = None, ): self.sku = sku self.quantity = quantity self.quantity_in_case = quantity_in_case self.prep_details_list = prep_details_list def _base_params_dict(self) -> dict: if not self.quantity_param: raise ValueError( f"{self.__class__.__name__}.quantity_param must be defined." ) data = { "SellerSKU": self.sku, self.quantity_param: self.quantity, "QuantityInCase": self.quantity_in_case, } # Each PrepDetails instance will parameterize itself, # but we need to enumerate it with "PrepDetailsList.member" if self.prep_details_list: parameterized_prep_details = [x.to_params() for x in self.prep_details_list] data.update( enumerate_keyed_param( "PrepDetailsList.member", parameterized_prep_details ) ) return data
[docs]class InboundShipmentPlanRequestItem(BaseInboundShipmentItem): """Item information for creating an inbound shipment plan. Submitted with a call to the CreateInboundShipmentPlan operation. `MWS docs: InboundShipmentPlanRequestItem Datatype <>`_ Adds the optional arguments ``asin`` (to include ASIN as needed) and ``condition`` (to add item condition information). ``condition`` may be a string or an instance of :py:class:`ItemCondition <mws.InboundShipments.ItemCondition>`. """ operations_permitted = ["CreateInboundShipmentPlan"] quantity_param = "Quantity" def __init__( self, *args, asin: str = None, condition: Union[ItemCondition, str] = None, **kwargs, ): super().__init__(*args, **kwargs) self.asin = asin self.condition = condition def params_dict(self) -> dict: data = self._base_params_dict() data.update( { "ASIN": self.asin, "Condition": self.condition, } ) return data
[docs]class InboundShipmentItem(BaseInboundShipmentItem): """Item information for an inbound shipment. Submitted with a call to the CreateInboundShipment or UpdateInboundShipment operation. `MWS docs: InboundShipmentItem Datatype <>`_ """ operations_permitted = ["CreateInboundShipment", "UpdateInboundShipment"] quantity_param = "QuantityShipped" def __init__( self, *args, release_date: datetime.datetime = None, **kwargs, ): super().__init__(*args, **kwargs) self.release_date = release_date self.fnsku = None def params_dict(self) -> dict: data = self._base_params_dict() data.update({"ReleaseDate": self.release_date}) return data
[docs] @classmethod def from_plan_item( cls, item: DotDict, quantity_in_case: int = None, release_date: datetime.datetime = None, ) -> "InboundShipmentItem": """Construct this model from a shipment plan returned from a CreateInboundShipmentPlan request. Expects a ``DotDict`` instance that can typically be found in the parsed response object by: 1. Iterating ``for plan in resp.parsed.InboundShipmentPlans.member:``; and 2. Iterating ``for item in plan.Items.member:``. Each ``item`` instance in the above example *should* work here [YMMV]. ``quantity_in_case`` must be passed manually for case-packed shipments, even when constructing from a shipment plan response, as this data is not typically returned in the plan details. ``release_date`` is also not part of a shipment plan response, so this must be passed manually in order to add it to the item. """ # Parse prep details from the plan object, if any exist. prep_details_list = [] if "PrepDetailsList" in item: for prep_details in item.PrepDetailsList.PrepDetails: prep_details_list.append( PrepDetails( prep_instruction=prep_details.PrepInstruction, prep_owner=prep_details.PrepOwner, ) ) # Construct the item model instance instance = cls( sku=item.SellerSKU, quantity=item.Quantity, quantity_in_case=quantity_in_case, prep_details_list=prep_details_list, release_date=release_date, ) # Add an FNSKU manually to this instance, if present in the plan data. instance.fnsku = item.get("FulfillmentNetworkSKU") return instance
[docs]class ExtraItemData: """Dataclass used for providing overrides to individual SKUs when processing items from a planned shipment in bulk using :py:func:`shipment_items_from_plan`. To utilize this data, construct a dictionary that maps SellerSKUs to instances of this class, then pass that dictionary to the ``overrides`` argument for ``shipment_items_from_plan``. Example: .. code-block:: python override_data = { # with a case quantity "MySku1": ExtraItemData(quantity_in_case=12), # a release date "MySku2": ExtraItemData(release_date=datetime.datetime(2021, 1, 28)), # or both (short version) "MySku3": ExtraItemData(24, datetime.datetime(2021, 1, 28)), } data = shipment_items_from_plan(plan, override_data) """ def __init__( self, quantity_in_case: int = None, release_date: datetime.datetime = None, ): self.quantity_in_case = quantity_in_case self.release_date = release_date def to_dict(self) -> dict: output = { "quantity_in_case": self.quantity_in_case, "release_date": self.release_date, } return {k: v for k, v in output.items() if v is not None}
[docs]def shipment_items_from_plan( plan: Union[DotDict, List[DotDict]], overrides: Dict[str, ExtraItemData] = None, ) -> List[InboundShipmentItem]: """Given a shipment plan response, returns a list of :py:class:`InboundShipmentItem <mws.InboundShipments.InboundShipmentItem>` models constructed from the contents of that plan's ``Items`` set. Expects ``plan`` to be a node from a parsed MWS response from the ``create_inbound_shipment_plan`` request, typically the ``resp.parsed.InboundShipmentPlans.member`` node (which may be a :py:class:`DotDict <mws.DotDict>` for a single plan or a list of ``DotDict`` instances for multiple; though both options should be natively iterable with the same interface). Providing ``overrides`` allows the addition of details that are not returned by ``create_inbound_shipment_plan``, such as ``quantity_in_case`` and ``release_date``. Expects a dict where SellerSKUs are keys and the values are either instances of ``ExtraItemData`` or dictionaries with the keys ``quantity_in_case`` and/or ``release_date``. Only items matching a SellerSKU key in ``overrides`` will have data overridden this way. """ overrides = overrides or {} if "member" in plan: # User has likely passed node ``InboundShipmentPlans``, but we need the child # node, ``member``. Move down to this node automatically. plan = plan.member if "Items" not in plan: raise ValueError( ( "'Items' node not found in plan. " "Requires a parsed response from the CreateInboundShipmentPlan request " "using the correct node in that response " "(typically `resp.parsed.InboundShipmentPlans.member`) " ) ) shipment_items = [] for item in plan.Items.member: # Gather override data from our overrides, if present override_data = overrides.get(item.SellerSKU, {}) if isinstance(override_data, ExtraItemData): # Convert from the ExtraItemData dataclass to a dict representation override_data = override_data.to_dict() # Narrow down override data to just the proper keys override_data = { k: v for k, v in override_data.items() if k in ("quantity_in_case", "release_date") } shipment_items.append(InboundShipmentItem.from_plan_item(item, **override_data)) return shipment_items