PKyOcdiscountapi/__init__.py""" cdiscountapi is a wrapper around the Cdiscount Marketplace API """ __version__ = "0.1.4" from cdiscountapi.cdiscountapi import Connection PK=bN%cdiscountapi/cdiscountapi.py# -*- coding: utf-8 -*- """ cdiscountapi.cdiscountapi ------------------------- :copyright: © 2019 Alexandria """ import lxml import requests from zeep import Client from zeep.plugins import HistoryPlugin from zeep.helpers import serialize_object from cdiscountapi.exceptions import CdiscountApiConnectionError from cdiscountapi.sections import ( Seller, Offers, Products, Orders, Relays, Fulfillment, WebMail, Discussions, ) from configparser import ConfigParser, ExtendedInterpolation from pathlib import Path import yaml # CONSTANTS DEFAULT_SITE_ID = 100 DEFAULT_CATALOG_ID = 1 DEFAULT_VERSION = "1.0" class Connection(object): """A class to manage the interaction with the CdiscountMarketplace API :param str login: The login :param str password: The password :param bool preprod: Whether we use the preprod (True) or the production environment (False) (default value: False) :param dict header_message: The header message :param str config: The path to a YAML config file Usage:: api = Connection(login, password, preprod, header_message=header_message, config=config) """ def __init__(self, login, password, preprod=False, header_message={}, config=""): self.preprod = preprod if self.preprod: domain = "preprod-cdiscount.com" else: domain = "cdiscount.com" self.wsdl = "https://wsvc.{0}/MarketplaceAPIService.svc?wsdl".format(domain) self.auth_url = ( "https://sts.{0}/users/httpIssue.svc/" "?realm=https://wsvc.{0}/MarketplaceAPIService.svc".format(domain) ) self.login = login self.password = password self.history = HistoryPlugin() self.client = Client(self.wsdl, plugins=[self.history]) self.factory = self.client.type_factory("http://www.cdiscount.com") if self.login is None or self.password is None: raise CdiscountApiConnectionError("Please provide valid login and password") self.token = self.get_token() if header_message != {} and config != "": raise CdiscountApiConnectionError( "You should provide header_message or config. Not both." ) if header_message: self.header = self.create_header_message(header_message) elif config: config_path = Path(config) if config_path.exists(): conf = yaml.load(config_path.read_text(), Loader=yaml.FullLoader) self.header = self.create_header_message(conf) else: raise CdiscountApiConnectionError( "Can't find the configuration file {}".format(config) ) else: raise CdiscountApiConnectionError( "You must provide header_message or config." ) # Instanciated sections. self.seller = Seller(self) self.offers = Offers(self) self.products = Products(self) self.orders = Orders(self) self.fulfillment = Fulfillment(self) self.relays = Relays(self) self.discussions = Discussions(self) self.webmail = WebMail(self) def get_token(self): response = requests.get(self.auth_url, auth=(self.login, self.password)) return lxml.etree.XML(response.text).text def _analyze_history(self, attr, error_msg): if len(self.history._buffer) == 0: return error_msg envelope = getattr(self.history, attr)["envelope"] return lxml.etree.tostring(envelope, pretty_print=True).decode("utf8") @property def last_request(self): """ Return the last SOAP request """ return self._analyze_history("last_sent", "No request sent.") @property def last_response(self): """ Return the last SOAP response """ return self._analyze_history("last_received", "No response received.") def create_header_message(self, data): messages_factory = self.client.type_factory( "http://schemas.datacontract.org/2004/07/" "Cdiscount.Framework.Core.Communication.Messages" ) # Set default values if they are not provided if "Context" in data: data["Context"].setdefault("SiteID", DEFAULT_SITE_ID) data["Context"].setdefault("CatalogID", DEFAULT_CATALOG_ID) else: data["Context"] = { "SiteID": DEFAULT_SITE_ID, "CatalogID": DEFAULT_CATALOG_ID, } if "Security" in data: data["Security"].setdefault("TokenId", self.token) else: data["Security"] = {"UserName": "", "TokenId": self.token} if "Version" not in data: data["Version"] = DEFAULT_VERSION return serialize_object(messages_factory.HeaderMessage(**data), dict) PKNNَAyycdiscountapi/exceptions.py# -*- coding: utf-8 -*- """ cdiscountapi.exceptions ------------------------- Define specific exceptions. :copyright: © 2019 Alexandria """ class CdiscountApiException(Exception): """""" pass class CdiscountApiConnectionError(CdiscountApiException): """""" pass class CdiscountApiTypeError(CdiscountApiException): """""" pass class CdiscountApiOrderError(CdiscountApiException): """ Raised when there's an error in the order """ class ValidationError(Exception): """ Raised when the parameters to create the Offers.xml or Products.xml are not valid """ PKce2OL$$cdiscountapi/helpers.py# -*- coding: utf-8 -*- """ cdiscountapi.helpers -------------------- Implements various helpers. :copyright: © 2019 Alexandria """ import json import os import zipfile from functools import wraps from pathlib import Path from shutil import ( copytree, rmtree, ) import zeep from cdiscountapi.packages import ( OfferPackage, ProductPackage, ) PRODUCT_CONDITIONS = { "LikeNew": 1, "VeryGoodState": 2, "GoodState": 3, "AverageState": 4, "Refurbished": 5, "New": 6, } def check_package_type(package_type): if package_type.lower() not in ("offer", "product"): raise ValueError('package_type must be either "offer" or "product".') # TODO Check the necessary files are present before adding them to the archive def make_package(package_type, path): """ Insert the files necessary for the offer/product in a zip file """ current_path = path.cwd() check_package_type(package_type) os.chdir(path) files = ( "Content/{}s.xml".format(package_type.capitalize()), "_rels/.rels", "[Content_Types].xml", ) with zipfile.ZipFile(path.parent / (path.name + ".zip"), "w") as zf: for f in files: zf.write(f, compress_type=zipfile.ZIP_DEFLATED) os.chdir(current_path) # TODO Print the name of the package after its creation # TODO Remove package_type. Determine package_type from the keys in data def generate_package(package_type, package_path, data, overwrite=True): """ Generate a zip package for the offers or the products Usage:: generate_package(package_type, package_path, data) Example:: # Generate Offer package: generate_package('offer', package_path, {'OfferCollection': offers, 'OfferPublicationList': offer_publications, 'PurgeAndReplace': purge_and_replace}) # Generate Product package: generate_package('product', package_path, {'Products': products}) :param str package_type: 'offer' or 'product' :param str package_path: the full path to the package (without .zip) :param dict data: offers or products as you can see on tests/samples/products/products_to_submit.json or tests/samples/offers/offers_to_submit.json """ check_package_type(package_type) package_path = Path(package_path) # The directory in which the package will be created must exist if not package_path.parent.exists(): raise FileNotFoundError( "The directory {} does not exist.".format(package_path.parent) ) if package_path.with_suffix(".zip").exists(): if overwrite: package_path.with_suffix(".zip").unlink() os.rmdir(package_path) else: raise FileExistsError("The package_path {} already exists.".format(package_path)) # Copy tree package. package_template = Path().joinpath( "cdiscountapi", "packages", f"{package_type}_package" ) package = copytree(package_template, package_path) xml_filename = package_type.capitalize() + "s.xml" # TODO Fix offer_dict # Add Products.xml from product_dict. with open(f"{package}/Content/{xml_filename}", "wb") as f: xml_generator = XmlGenerator(data) f.write(xml_generator.generate().encode("utf8")) make_package(package_type, package_path) rmtree(package_path) print("Successfully created {}.zip".format(package_path)) def check_element(element_name, dynamic_type): """ Raise an exception if the is not in the dynamic_type Example >>> check_element('CarrierName', api.factory.ValidateOrder) """ valid_elements = [x[0] for x in dynamic_type.elements] if element_name not in valid_elements: raise TypeError( f"{element_name} is not a valid element of {dynamic_type.name}." f" Valid elements are {valid_elements}" ) # TODO Damien: voir car l'utilisateur peut écrire # "Shipping Fees" ou "ShippingFees" au lieu de "shipping_fees" def get_motive_id(label): label_to_motive_id = { "compensation_on_missing_stock": 131, "product_delivered_damaged": 132, "product_delivered_missing": 132, "error_of_reference": 133, "error_of_color": 133, "error_of_size": 133, "fees_unduly_charged_to_the_customer": 134, "late_delivery": 135, "product_return_fees": 136, "shipping_fees": 137, "warranty_period_passed": 138, "rights_of_withdrawal_passed": 138, "others": 139, } if label not in label_to_motive_id: raise KeyError( "Please choose a valid label ({})".format(list(label_to_motive_id)) ) return label_to_motive_id[label] # TODO Make sure the exceptions is well chosen for an outdated token def auto_refresh_token(func): """ Refresh the token when it's outdated and resend the request """ @wraps(func) def wrapper(*args, **kwargs): self = args[0] try: return func(*args, **kwargs) except zeep.exceptions.Fault: print("Refreshing token...") self.api.token = self.api.get_token() self.api.header["Security"]["TokenId"] = self.api.token print("Resending request...") return func(*args, **kwargs) return wrapper class XmlGenerator(object): """ Generate offers or products to upload Usage:: xml_generator = XmlGenerator(data, preprod=preprod) content = xml_generator.generate() Example:: # Render the content of Offers.xml shipping_info1 = { 'AdditionalShippingCharges': 1, 'DeliveryMode': 'RelaisColis', 'ShippingCharges': 1, } shipping_info2 = { 'AdditionalShippingCharges': 5.95, 'DeliveryMode': 'Tracked', 'ShippingCharges': 2.95 } discount_component = { 'StartDate': datetime.datetime(2019, 11, 23), 'EndDate': datetime.datetime(2019, 11, 25), 'Price': 85, 'DiscountValue': 1, 'Type': 1 } offer = { 'ProductEan': 1, 'SellerProductId': 1, 'ProductCondition': '6' 'Price': 100, 'EcoPart': 0, 'Vat': 0.19, 'DeaTax': 0, 'Stock': 1, 'Comment': 'Offer with discount Tracked or RelaisColis' 'PreparationTime': 1, 'PriceMustBeAligned': 'Align', 'ProductPackagingUnit': 'Kilogram', 'ProductPackagingValue': 1, 'MinimumPriceForPriceAlignment': 80, 'StrikedPrice': 150, 'DiscountList': {'DiscountComponent': [discount_component]}, 'ShippingInformationList': {'ShippingInformation': [shipping_info1, shipping_info2]} } offers_xml = XmlGenerator({'OfferCollection': [offer], 'PurgeAndReplace': False, 'OfferPublicationList': [1, 16], 'Name': 'A package name', 'PackageType': "Full", }, preprod=preprod) content = offers_xml.generate() # Render the content of Products.xml products_xml = XmlGenerator({'Products': [product]}, preprod=preprod) content = products_xml.generate() """ def __init__(self, data, preprod=False): if OfferPackage.has_required_keys(data): self.package = OfferPackage(data, preprod) elif ProductPackage.has_required_keys(data): self.package = ProductPackage(data, preprod) else: msg = ( "The data should be a dictionary with the keys {offers} for" "Offers.xml and {products} for Products.xml".format( offers=OfferPackage.required_keys, products=ProductPackage.required_keys, ) ) raise ValueError(msg) self.data = self.package.data def add(self, data): self.package.add(data) def generate(self): return self.package.generate() def analyze_offer_report_property_log(response): """ Return the meaning of the PropertyCode and PropertyError in the node OfferReportPropertyLog returned by GetOfferPackageSubmissionResult """ with open("cdiscountapi/assets/offer.json", "r") as f: codes = json.load(f) meanings = [] for offer_report_property_log in response["OfferLogList"]["OfferReportLog"][ "PropertyList" ]: meanings.append( { "PropertyCode": codes["property_codes"].get( offer_report_property_log["PropertyCode"], "" ), "PropertyError": codes["error_codes"].get( offer_report_property_log["PropertyError"], "" ), } ) return meanings PKYNcdiscountapi/.tox/.package.lockPKNNHwcdiscountapi/assets/config.yamlVersion: '1.0' Context: SiteID: 100 CatalogID: 1 Localization: Country: 'Fr' Currency: 'Eur' DecimalPosition: 2 Security: UserName: '' PKNNvtcdiscountapi/assets/offer.json{ "property_codes": { "0": "", "1": "EAN", "2": "Product status", "3": "Seller product reference", "4": "SKU", "5": "Stock", "6": "Price", "7": "VAT", "8": "Eco part", "9": "Min shipping time", "10": "Max shipping time", "11": "Shipping fee - Standard mode", "12": "Shipping fee - Tracked mode", "13": "Shipping fee - Registered mode", "14": "Offers comment", "15": "Additional Shipping fee - Standard mode", "16": "Additional Shipping fee - Tracked mode", "17": "Additional Shipping fee - Registered mode", "18": "Reference price", "19": "Promotion type", "20": "Promotion type - Start date", "21": "Promotion type - Start hour", "22": "Promotion type - End date", "23": "Package", "24": "Product", "25": "Promotion type - End Hour", "26": "Discount %", "27": "Sales Reference Price", "28": "Offer", "29": "Delivery charges - Mode Immediate Withdrawal", "30": "Additional Delivery Fee - Immediate Withdrawal Mode", "31": "DEA", "40": "Store delivery", "41": "Store delivery - Min shipping time", "42": "Store delivery - Max shipping time", "43": "Store delivery - shipping fee", "44": "Store delivery - Additional shipping fee", "45": "Home delivery", "46": "Lowest allowable price", "48": "Mondial Relay pick up - Min shipping time", "49": "Mondial Relay pick up - Shipping fee", "50": "Mondial Relay pick up - Shipping fee", "51": "Mondial Relay pick up - Additional shipping fee", "52": "Relais colis pick up", "53": "Relais colis pick up - Min shipping time", "54": "Relais colis pick up - Max shipping time", "55": "Relais colis pick up - Shipping fee", "56": "Relais colis pick up - Additional shipping fee", "57": "SoColissimo pick up", "58": "SoColissimo pick up - Min shipping time", "59": "SoColissimo pick up - Max shipping time", "60": "SoColissimo pick up - Shipping fee", "61": "SoColissimo pick up - Additional shipping fee", "62": "Packaging unit", "63": "Product packaging", "64": "Fullfilment" }, "error_codes": { "10": "Required field, please enter data", "11": "Incorrect data format, check the required format", "12": "Data incorrect length, check the required format", "13": "Non existent - Request its creation in the 'product creation' section of the seller interface", "14": "Product already known", "15": "Ask permission to sell this product to the seller support", "16": "Invalid key, check EAN", "17": "Incorrect value as greater than the maximum value, check the consistency of values", "17": "Incorrect value as lower than the minimum value, check the consistency of values", "18": "Not enough stock relative to pending orders", "19": "Incorrect value, price lower than or equal to €0", "20": "Incorrect value, reference price lower than 0,10€", "21": "Error occurred during integration", "23": "Incorrect value, reference price lower than the sale price", "24": "A offer already exists for this produit with another reference, impossible to modify, please use the old reference", "25": "Seller reference already used for another EAN or another produit condition. Please change your reference", "26": "End date/time earlier than Start date/time", "27": "Products with variations (size/colour): to be created for your seller account in the 'product creation'", "28": "Offer already submitted and awaiting processing", "29": "Price {0} is an amount lower than the eco part + DEA", "30": "Reference unknown. You must create your product before putting an offer, or put an offer on an existing product using the EAN", "31": "You have several products with the reference seller. Please use the EAN", "32": "Off (being activated), the offer is on hold, it will be published in the hour", "34": "Existing but not open to the marketplace. Please contact the sellersupport", "35": "Value greater than the permitted ceiling 30.00", "36": "This offer does not exist and can not be updated. You must first create", "39": "Fulfillment: Product dereferenced of Fulfillment with stock> 0. You must take all your products on our warehouse before reactivating this offer", "40": "Not configured - You must set the {0} Mode 'in the section' Your shipping choices 'before entering the file data'", "43": "Not accessible to your subscription", "44": "Thank you to first take back your stock ....", "45": "Non-salable only", "48": "Several eans match the product, please contact the account manager", "80": "Offer created", "90": "Offer updated" } } PKyO""!cdiscountapi/packages/__init__.py# -*- coding: utf-8 -*- """ cdiscountapi.packages --------------------- Implements the Offers.xml and Products.xml content generation. :copyright: © 2019 Alexandria """ # Python imports from copy import deepcopy # Third-party imports import zeep from jinja2 import Environment, FileSystemLoader from cdiscountapi.packages.validator import ( OfferValidator, ProductValidator, DiscountComponentValidator, ShippingInformationValidator, ProductEanValidator, ProductImageValidator, ) class BasePackage(object): required_keys = [] def __init__(self, preprod=False): self.preprod = preprod if self.preprod: domain = "preprod-cdiscount.com" else: domain = "cdiscount.com" self.wsdl = "https://wsvc.{0}/MarketplaceAPIService.svc?wsdl".format(domain) self.client = zeep.Client(self.wsdl) self.factory = self.client.type_factory("http://www.cdiscount.com") self.data = [] def validate(self, **kwargs): raise NotImplementedError def generate(self): raise NotImplementedError @classmethod def has_required_keys(cls, data): """ Return True if the data passed to the package have the required keys """ for required_key in cls.required_keys: if required_key not in data.keys(): return False return True class OfferPackage(BasePackage): required_keys = ["OfferCollection"] def __init__(self, data, preprod=False): super().__init__(preprod=preprod) self.check_offer_publication_list(data.get("OfferPublicationList")) self.purge_and_replace = data.get("PurgeAndReplace", False) self.name = data.get("Name", "A package") self.package_type = data.get("PackageType", "Full") self.add(data["OfferCollection"]) def check_offer_publication_list(self, ids): """ The offer_publication_list should be a list of integers representing the id of the marketplaces if it exists. """ if not ids: self.offer_publication_list = [] return None msg = ( "The value OfferPublicationList should be a list of" " integers representing the ids of the marketplaces." ) if not isinstance(ids, (list, tuple)): raise TypeError(msg) for _id in ids: if not isinstance(_id, int): raise TypeError(msg) self.offer_publication_list = [{"Id": _id} for _id in ids] def add(self, offers): for offer in offers: valid_offer = self.validate(**offer) if valid_offer not in self.data: self.data.append(valid_offer) def extract_from(self, offer, attr1, attr2): """ Extract the elements of a list from Offer (ex: the ShippingInformation elements in ShippingInformationList, the DiscountComponent elements in DiscountList...) """ if attr1 in offer: sub_record = offer.get(attr1, None) if sub_record: datum = sub_record[attr2] del offer[attr1] return datum else: return [] else: return [] def validate(self, **kwargs): """ Return the valid offer Usage:: offer_package.validate(**kwargs) """ new_kwargs = kwargs.copy() # We validate the lists in Offer if "DiscountList" in kwargs: new_kwargs["DiscountList"] = { "DiscountComponent": [ DiscountComponentValidator.validate(x) for x in new_kwargs["DiscountList"]["DiscountComponent"] ] } if "ShippingInformationList" in kwargs: new_kwargs["ShippingInformationList"] = { "ShippingInformation": [ ShippingInformationValidator.validate(x) for x in new_kwargs["ShippingInformationList"][ "ShippingInformation" ] ] } return OfferValidator.validate(new_kwargs) def generate(self): loader = FileSystemLoader("cdiscountapi/templates") env = Environment(loader=loader, trim_blocks=True, lstrip_blocks=True) template = env.get_template("Offers.xml") offers = deepcopy(self.data) extraction_mapping = { "shipping_information_list": ( "ShippingInformationList", "ShippingInformation", ), "discount_list": ("DiscountList", "DiscountComponent"), } offers_data = [] for offer in offers: offers_datum = {} for key, (attr1, attr2) in extraction_mapping.items(): # Only add if the user provided the attribute if attr1 in offer: if key not in offers_datum and attr1 in offer: offers_datum[key] = [] offers_datum[key].extend(self.extract_from(offer, attr1, attr2)) if "attributes" not in offers_datum: offers_datum["attributes"] = "" # We keep only key:value pairs whose values are not None offers_datum["attributes"] += " ".join( '{}="{}"'.format(k, v) for k, v in offer.items() if v is not None ) offers_data.append(offers_datum) return template.render( offers=offers_data, offer_publication_list=self.offer_publication_list, purge_and_replace=self.purge_and_replace, package_type=self.package_type, name=self.name ) class ProductPackage(BasePackage): required_keys = ["Products"] def __init__(self, data, preprod=False): super().__init__(preprod=preprod) self.add(data["Products"]) def add(self, products): for product in products: valid_product = self.validate(**product) if valid_product not in self.data: self.data.append(valid_product) def extract_from(self, product, attr1, attr2): """ Extract the elements of a list from Offer (ex: the ShippingInformation elements in ShippingInformationList, the DiscountComponent elements in DiscountList...) """ if attr1 in product: sub_record = product.get(attr1, None) if sub_record: datum = sub_record[attr2] del product[attr1] return datum else: return [] else: return [] def validate(self, **kwargs): new_kwargs = kwargs.copy() if "EanList" in kwargs: new_kwargs["EanList"] = { "ProductEan": [ ProductEanValidator.validate(x) for x in new_kwargs["EanList"]["ProductEan"] ] } if "Pictures" in kwargs: new_kwargs["Pictures"] = { "ProductImage": [ ProductImageValidator.validate(x) for x in new_kwargs["Pictures"]["ProductImage"] ] } return ProductValidator.validate(new_kwargs) def generate(self): loader = FileSystemLoader("cdiscountapi/templates") env = Environment(loader=loader, trim_blocks=True, lstrip_blocks=True) template = env.get_template("Products.xml") products = deepcopy(self.data) extraction_mapping = { "EanList": ("EanList", "ProductEan"), "Pictures": ("Pictures", "ProductImage"), } products_data = [] for product in products: products_datum = {} for key, (attr1, attr2) in extraction_mapping.items(): if key not in products_datum: products_datum[key] = [] products_datum[key].extend(self.extract_from(product, attr1, attr2)) if "ModelProperties" in product: products_datum["ModelProperties"] = product["ModelProperties"] del product["ModelProperties"] if "attributes" not in products_datum: products_datum["attributes"] = "" # We keep only key:value pairs whose values are not None products_datum["attributes"] += " ".join( '{}="{}"'.format(k, v) for k, v in product.items() if v is not None ) products_data.append(products_datum) capacity = sum(len(p['Pictures']) for p in products_data) return template.render(products=products_data, capacity=capacity) PKNNd(`  "cdiscountapi/packages/validator.py# Project imports from cdiscountapi.exceptions import ValidationError class BaseValidator(object): required = set() optional = set() @classmethod def package_type(cls): return cls.__name__.split("Validator")[0] @classmethod def validate(cls, data): """ Return the data if the keys are valid attributes """ provided = set(data.keys()) missing_required = cls.required - provided if len(missing_required) > 0: raise ValidationError( "Missing required attributes for {}: {}".format( cls.package_type(), missing_required ) ) invalid_attributes = provided - cls.required - cls.optional if len(invalid_attributes) > 0: raise ValidationError( "These attributes are not valid: {}." " Please use only the following ones if necessary: {}".format( invalid_attributes, cls.optional ) ) return data class OfferValidator(BaseValidator): required = { "ProductEan", "SellerProductId", "ProductCondition", "Price", "EcoPart", "Vat", "DeaTax", "Stock", "PreparationTime", } optional = { "Comment", "StrikedPrice", "PriceMustBeAligned", "MinimumPriceForPriceAlignment", "ProductPackagingUnit", "ProductPackagingValue", "BluffDeliveryMax", "DiscountList", "ShippingInformationList", } class ProductValidator(BaseValidator): required = { "ShortLabel", "SellerProductId", "CategoryCode", "ProductKind", "Model", "LongLabel", "Description", "BrandName", "EanList", "Pictures", } optional = { "Width", "Weight", "Size", "SellerProductFamily", "SellerProductColorName", "ManufacturerPartNumber", "Length", "ISBN", "Height", "EncodedMarketingDescription", "ModelProperties", "Navigation", } class DiscountComponentValidator(BaseValidator): required = {"DiscountValue", "Type", "StartDate", "EndDate"} class ShippingInformationValidator(BaseValidator): required = {"ShippingCharges", "AdditionalShippingCharges", "DeliveryMode"} class ProductEanValidator(BaseValidator): required = {"Ean"} class ProductImageValidator(BaseValidator): required = {"Uri"} PKNNڸPN7cdiscountapi/packages/offer_package/[Content_Types].xml PKNN6cdiscountapi/packages/offer_package/Content/Offers.xmlPKzOzQg/cdiscountapi/packages/offer_package/_rels/.rels PKNNpWNN9cdiscountapi/packages/product_package/[Content_Types].xml PKNN:cdiscountapi/packages/product_package/Content/Products.xmlPKNNGj>1cdiscountapi/packages/product_package/_rels/.rels PKNN^!cdiscountapi/sections/__init__.py# -*- coding: utf-8 -*- """ cdiscountapi.sections --------------------- The different sections available to interact with the API. :copyright: © 2019 Alexandria """ from .seller import Seller from .offers import Offers from .products import Products from .orders import Orders from .relays import Relays from .fulfillment import Fulfillment from .webmail import WebMail from .discussions import Discussions PKNNsPZD||cdiscountapi/sections/base.py# -*- coding: utf-8 -*- """ cdiscountapi.sections.base -------------------------- Base class for the different section classes :copyright: © 2019 Alexandria """ from zeep.helpers import serialize_object class BaseSection(object): def __init__(self, api): self.api = api self.arrays_factory = self.api.client.type_factory( "http://schemas.microsoft.com/2003/10/Serialization/Arrays" ) def array_of(self, type_name, sequence): """ Cast the sequence into an array of the given type. The arrays are defined in the XSD file (cf http://schemas.microsoft.com/2003/10/Serialization/Arrays) (cf https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-ipamm/38d7c101-385d-4180-bb95-983955f41e19) ? """ valid_type_names = ( "int", "string", "long", "KeyValueOfstringArrayOfstringty7Ep6D1", "KeyValueOfintstring", ) if type_name not in valid_type_names: raise TypeError( "Invalid type_name. " "Please choose between {}".format(valid_type_names) ) array = getattr(self.arrays_factory, "ArrayOf{}".format(type_name))(sequence) return serialize_object(array, dict) def update_with_valid_array_type(self, record, keys_to_cast): """ Update the dictionary with the array having the valid type :param dict record: The dictionary to update :param dict keys_to_cast: The dictionary specifying what keys to convert and what types Example:: section = BaseSection(api) new_record = section.update_with_valid_array_type( {'DepositIdList': [1, 2, 3], 'PageSize': 10}, {'DepositIdList': 'int'} ) :returns: The updated dictionary with the valid array type """ new_record = record.copy() for key, type_name in keys_to_cast.items(): if key in new_record: new_record[key] = self.array_of(type_name, new_record[key]) return new_record PKNNQQYY$cdiscountapi/sections/discussions.py# -*- coding: utf-8 -*- """ cdiscountapi.sections.discussions --------------------------------- Handles the discussions. :copyright: © 2019 Alexandria """ from zeep.helpers import serialize_object from .base import BaseSection from ..helpers import auto_refresh_token class Discussions(BaseSection): """ There are 3 ways to get discussions: discussions id, the discussions status, and all messages. Thanks to the discussion Id and the method GetDiscussionMailList you can get an encrypted mail address to reply to a question or a claim. You can close a discussion list with the method CloseDiscussionList and the Discussion id, you cannot close a discussion without having answered. Methods:: get_order_claim_list(**order_claim_filter) get_offer_question_list(**offer_question_filter) get_order_question_list(**order_question_filter) close_discussion_list(discussion_ids) Operations are included in the Discussions API section. (https://dev.cdiscount.com/marketplace/?page_id=148) """ @auto_refresh_token def get_order_claim_list(self, **order_claim_filter): """ Return the list of order claims """ order_claim_filter = self.api.factory.OrderClaimFilter(**order_claim_filter) response = self.api.client.service.GetOrderClaimList( headerMessage=self.api.header, orderClaimFilter=order_claim_filter ) return serialize_object(response, dict) @auto_refresh_token def get_offer_question_list(self, **offer_question_filter): """ Return the list of questions about offers with the specified criteria :param offer_question_filter: The keywords for the filter ``offerQuestionFilter``: - BeginCreationDate - BeginModificationDate - EndCreationDate - EndModificationDate - StatusList - DiscussionStateFilter can have the values - All - Open - Closed - NotProcessed - ProductEANList - ProductSellerReferenceList Example:: response = api.get_offer_question_list( StatusList={'DiscussionStateFilter': 'Open'}, BeginCreationDate='2019-01-01' ) :returns: An OfferQuestionListMessage dictionary. .. note:: A date is mandatory in the query. """ offer_question_filter = self.api.factory.OfferQuestionFilter( **offer_question_filter ) response = self.api.client.service.GetOfferQuestionList( headerMessage=self.api.header, offerQuestionFilter=offer_question_filter ) return serialize_object(response, dict) @auto_refresh_token def get_order_question_list(self, **order_question_filter): """ Return the list of questions about orders with the specified criteria :param order_question_filter: The keywords for the filter ``orderQuestionFilter``: - BeginCreationDate - BeginModificationDate - EndCreationDate - EndModificationDate - StatusList - DiscussionStateFilter can have the values - All - Open - Closed - NotProcessed Example:: response = api.get_order_question_list( StatusList={'DiscussionStateFilter': 'Open'}, BeginCreationDate='2019-01-01' ) :returns: An OrderQuestionListMessage dictionary. .. note:: A date is mandatory in the query. """ order_question_filter = self.api.factory.OrderQuestionFilter( **order_question_filter ) response = self.api.client.service.GetOrderQuestionList( headerMessage=self.api.header, orderQuestionFilter=order_question_filter ) return serialize_object(response, dict) @auto_refresh_token def close_discussion_list(self, discussion_ids): """ Close a discussion list :param list discussion_ids: The list of discussion_ids to close Usage:: response = api.discussions.close_discussion_list([31, 4, 159]) """ close_discussion_request = self.api.factory.CloseDiscussionRequest( DiscussionIds=self.array_of("long", discussion_ids) ) response = self.api.client.service.CloseDiscussionList( headerMessage=self.api.header, closeDiscussionRequest=close_discussion_request, ) return serialize_object(response, dict) PKNNm55$cdiscountapi/sections/fulfillment.py# -*- coding: utf-8 -*- """ cdiscountapi.sections.fulfillment --------------------------------- Handles the supply order fulfillment. :copyright: © 2019 Alexandria """ from zeep.helpers import serialize_object from .base import BaseSection from ..helpers import auto_refresh_token, check_element class Fulfillment(BaseSection): """ Allows to manage the fulfillment of the supply orders. Methods:: submit_fulfillment_supply_order(prod_desc_list) submit_fulfillment_on_demand_supply_order(order_list) get_fulfillment_supply_order_report_list(**request) get_fulfillment_delivery_document(deposit_id) get_fulfillment_supply_order submit_offer_state_action(request) get_fulfillment_activation_report_list(**request) get_fulfillment_order_list_to_supply(**request) submit_offer_state_action(**request) get_external_order_status(**request) get_product_stock_list(**request): Operations are included in the Fulfillment API section (https://dev.cdiscount.com/marketplace/?page_id=2222) """ @auto_refresh_token def submit_fulfillment_supply_order(self, prod_desc_list): """ This operation create a deposit that will asynchronously triggered supply order creation. :param prod_desc_list: list of FulfilmentProductDescription: - ProductEAN *(str)*: [Mandatory] - Warehouse *(str)*: [Mandatory] - "CEM" - "ANZ" - "SMD" - Quantity *(int)*: [Mandatory] - ExtSupplyOrderID *(str)*: Seller external supply order reference - WarehouseReceptionMinDate *(date)* - SellerProductReference *(str)* Example:: response = api.fulfillment.submit_fulfillment_supply_order( [ { 'ProductEAN': '1051248556517', 'Warehouse': 'SMD' 'Quantity': 3, }, { 'ProductEAN': '1234567891234', 'Warehouse': 'ANZ', 'Quantity': 122, 'SellerProductReference': '154' } ] ) :return: """ for desc in prod_desc_list: check_element(desc, self.api.factory.FulfilmentProductDescription) response = self.api.client.service.SubmitFulfilmentSupplyOrder( headerMessage=self.api.header, request=prod_desc_list ) return serialize_object(response, dict) @auto_refresh_token def submit_fulfillment_on_demand_supply_order(self, order_list): """ :param order_list: list of dict as: [{'OrderReference': 1703182124BNXCO,'ProductEan': 2009854780777}] Example:: response = api.fulfillment.submit_fulfillment_on_demand_supply_order( order_list= [ { 'OrderReference': '1703182124BNXCO', 'ProductEan': 2009854780777 }, { 'OrderReference': '2813182124AMYBP', 'ProductEan': '3009854780789' } ] """ for order in order_list: check_element(order, self.api.factory.FulfilmentOrderLineRequest) response = self.api.client.service.SubmitFulfilmentOnDemandSupplyOrder( headerMessage=self.api.header, request={"OrderLineList": order_list} ) return serialize_object(response, dict) @auto_refresh_token def get_fulfillment_supply_order_report_list(self, **request): """ To search supply order reports. :param date BeginCreationDate: :param list DepositIdList: list of ints :param date EndCreationDate: :param int PageNumber: :param int PageSize: Examples:: response = api.fulfillment.get_fulfillment_supply_order_report_list( PageSize=10, BeginCreationDate=datetime.datetime(2019, 1, 1), EndCreationDate=datetime.datetime(2019, 1, 2), ) response = api.fulfillment.get_fulfillment_supply_order_report_list( PageSize=10, DepositIdList=[1, 2, 3] ) :return: supply order reports """ request = self.update_with_valid_array_type(request, {"DepositIdList": "int"}) supply_order_report_request = self.api.factory.SupplyOrderReportRequest( **request ) response = self.api.client.service.GetFulfilmentSupplyOrderReportList( headerMessage=self.api.header, request=supply_order_report_request ) return serialize_object(response, dict) @auto_refresh_token def get_fulfillment_delivery_document(self, deposit_id): """ To get pdf document data for a supply order delivery, in the form of a Base64-encoded string. :param int deposit_id: Unique identification number of the supply order request Usage:: response = api.fulfillment.get_fulfillment_delivery_document(233575) :return: data for printing PDF documents, in the form of a Base64-encoded string. """ response = self.api.client.service.GetFulfilmentDeliveryDocument( headerMessage=self.api.header, request={"DepositId": deposit_id} ) return serialize_object(response, dict) @auto_refresh_token def get_fulfillment_supply_order(self, **request): """ :param int PageSize: :param date BeginModificationDate: :param date EndModificationDate: :param int PageNumber: :param list SupplyOrderNumberList: list of strings Examples:: response = api.fulfillment.get_fulfillment_supply_order( PageSize=10, BeginCreationDate=datetime.datetime(2019, 1, 1), EndCreationDate=datetime.datetime(2019, 1, 2), ) response = api.fulfillment.get_fulfillment_supply_order( SupplyOrderNumberList=['X', 'Y', 'Z'] ) :return: supply orders """ request = self.update_with_valid_array_type( request, {"SupplyOrderNumberList": "string"} ) supply_order_report_request = self.api.factory.SupplyOrderRequest(**request) response = self.api.client.service.GetFulfilmentSupplyOrder( headerMessage=self.api.header, request=supply_order_report_request ) return serialize_object(response, dict) # TODO Make this method more robust @auto_refresh_token def submit_fulfillment_activation(self, request): """ To ask for products activation (or deactivation) :param list ProductList: - Action *(str)*: - 'Activation' - 'Deactivation' - Length *(double/float64)*: - Width *(double/float64)*: - Height *(double/float64)*: - Weight *(double/float64)*: - ProductEAN *(str)*: - SellerProductReference *(str)*: [optional] Example:: api.fulfillment.submit_fulfillment_activation( product_list = [ { 'Action': 'Activation', 'Height': 1, 'Length': 20, 'ProductEAN': '2009863600561' 'Weight': 50, 'Width': 10 }, { 'Action': 'Activation', 'Height': 1, 'Length': 20, 'ProductEAN': 'BZ34567891234', 'Weight': 20, 'Width': 10 ) :return: deposit id """ response = self.api.client.service.SubmitFulfilmentActivation( headerMessage=self.api.header, request=request ) return serialize_object(response, dict) @auto_refresh_token def get_fulfillment_activation_report_list(self, **request): """ To get status and details about fulfillment products activation. :param str BeginDate: date :param list DepositIdList: int list :param str EndDate: date Example:: response = api.fulfillment.get_fulfillment_activation_report_list( BeginDate='2019-04-26T09:54:38:72' ) """ activation_report_request = self.api.factory.FulfilmentActivationReportRequest( **request ) response = self.api.client.service.GetFulfilmentActivationReportList( headerMessage=self.api.header, request=activation_report_request ) return serialize_object(response, dict) @auto_refresh_token def get_fulfillment_order_list_to_supply(self, **request): """ To ask for fulfillment on demand order lines to supply. :param str OrderReference: :param str ProductEan: :param str Warehouse: name of warehouses where products are stored: - 'CEM' - 'ANZ' - 'SMD' Example:: response = api.fulfillment.get_fulfillment_order_list_to_supply( ProductEan='2009863600561' ) :return: fulfillment on demand order lines to supply answering the search criterion. """ references = self.api.factory.FulfilmentOnDemandOrderLineFilter(**request) response = self.api.client.service.GetFulfilmentActivationReportList( headerMessage=self.api.header, request=references ) return serialize_object(response, dict) @auto_refresh_token def submit_offer_state_action(self, **request): """ To set an offer online or offline :param str Action: 'Publish' or 'Unpublish' :param str SellerProductId: Usage:: response = api.fulfillment.submit_offer_state_action( Action='Unpublish', SellerProductId='11504' ) :return: """ seller_action = self.api.factory.OfferStateActionRequest(**request) response = self.api.client.service.SubmitOfferStateAction( headerMessage=self.api.header, offerStateRequest=seller_action ) return serialize_object(response, dict) @auto_refresh_token def create_external_order(self, **order): """ create an order from another marketplace. :param order: - Example:: response = api.fufillment.create_external_order( Comments: str, Corporation: str (ex:FNAC), Customer: customer info dict, CustomerOrderNumber: str, OrderDate: date, ShippingMode: str, OrderLineList: [ 'ExternalOrderLine': { 'ProductEan': str, 'ProductReference': str, 'Quantity': int } ], } :return: The response """ response = self.api.client.service.CreateExternalOrder( headerMessage=self.api.header, request={"Order": order} ) return serialize_object(response, dict) @auto_refresh_token def get_external_order_status(self, **request): """ to get order integration status :param str Corporation: website from which the order comes. :param str CustomerOrderNumber: Usage:: response = api.fufillment.get_external_order_status( Corporation='FNAC', CustomerOrderNumber='100-00110101-10011100' ) :return: Status ("OK", "Pending", "KO") """ response = self.api.client.service.GetExternalOrderStatus( headerMessage=self.api.header, request={ "Corporation": request.get("Corporation"), "CustomerOrderNumber": request.get("CustomerOrderNumber"), }, ) return serialize_object(response, dict) @auto_refresh_token def get_product_stock_list(self, **request): """ List seller product :param list BarCodeList: list of EAN :param str FulfilmentReferencement: - 'All' - 'OnlyReferenced' - 'OnlyNotReferenced' :param str ShippableStock: - 'All' - 'WithStock' - 'WithoutStock' - 'ShippableStock' :param str BlockedStock: - 'All' - 'WithStock' - 'WithoutStock' - 'BlockedStock' :param str SoldOut: - 'None' - 'All' - 'InSoldOut' - 'InSoldOutFiveDays' - 'InSoldOutFifteenDays' - 'SoldOut' Example:: response = api.fulfillment.get_product_stock_list( FulfilmentReferencement='OnlyReferenced', ShippableStock='WithStock', BlockedStock='All', SoldOut='InSoldOut' ) :return: ProductStockList, Status ("OK", "NoData", "KO") and TotalProductCount """ response = self.api.client.service.GetProductStockList( headerMessage=self.api.header, request=request ) return serialize_object(response, dict) PK*{"O`W!!cdiscountapi/sections/offers.py# -*- coding: utf-8 -*- """ cdiscountapi.sections.offers ---------------------------- Handles the offers. :copyright: © 2019 Alexandria """ from zeep.helpers import serialize_object from cdiscountapi.helpers import generate_package from .base import BaseSection from ..helpers import auto_refresh_token class Offers(BaseSection): """ Offers section lets sellers retrieve information about their offers. Methods:: get_offer_list(**filters) get_offer_list_paginated(**filters) generate_offer_package(package_name, offers_list, offer_publication_list=offer_publication_list, purge_and_replace=purge_and_replace) submit_offer_package(url) get_offer_package_submission_result(package_id) Operations are included in the Products API section (https://dev.cdiscount.com/marketplace/?page_id=84) """ @auto_refresh_token def get_offer_list(self, **filters): """ To search offers. This operation seeks offers according to the following criteria: - SellerProductIdList: list of seller product references - OfferPoolId (int) is the distribution website Id Example:: response = api.offers.get_offer_list( SellerProductIdList=['REF1', 'REF2', 'REF3'], OfferPoolId=1 ) :return: offers answering the search criterion """ filters = self.update_with_valid_array_type( filters, {"SellerProductIdList": "string"} ) offer_filter = self.api.factory.OfferFilter(**filters) response = self.api.client.service.GetOfferList( headerMessage=self.api.header, offerFilter=offer_filter ) return serialize_object(response, dict) @auto_refresh_token def get_offer_list_paginated(self, **filters): """ Recovery of the offers page by page. - PageNumber (int) [mandatory] - OfferFilterCriterion (str): - 'NewOffersOnly' - 'UsedOffersOnly' - OfferPoolId (int) is the distribution website Id - OfferSortOrder (str): - ByPriceAscending - ByPriceDescending - BySoldQuantityDescending - ByCreationDateDescending - OfferStateFilter (str): - WaitingForProductActivation - Active - Inactive - Archived - Fulfillment - SellerProductIdList (list of str) Example:: response = api.offers.get_offer_list_paginated( PageNumber=1, OfferFilterCriterion='NewOffersOnly', OfferSortOrder='BySoldQuantityDescending', OfferStateFilter='Active' ) :return: offers answering the search criterion """ filters = self.update_with_valid_array_type( filters, {"SellerProductIdList": "string"} ) offer_filter = self.api.factory.OfferFilterPaginated(**filters) response = self.api.client.service.GetOfferListPaginated( headerMessage=self.api.header, offerFilter=offer_filter ) return serialize_object(response, dict) @staticmethod def generate_offer_package( package_name, package_path, offers_list, package_type="Full", offer_publication_list=[], purge_and_replace=False, overwrite=True ): """ Generate a zip offers package as cdiscount wanted. :param str package_name: The name of the package :param str package_path: [mandatory] the full path to the offer package (without .zip) :param str package_type: [optional] The type of package ("Full" or "StockAndPrice") (default: "Full") :param list offer_publication_list: [optional] :param bool purge_and_replace: [optional] :param bool overwrite: [optional] Determine if an existing package is overwritten when a new one with the same name is created (default: True) :param list offers_list: list of dict [{offer, shipping}, ...]: -Offer: - Mandatory attributes: - ProductEan *(str)* - SellerProductId *(str)* - ProductCondition *(int)*: - 1: 'LikeNew', - 2: 'VeryGoodState', - 3: 'GoodState', - 4: 'AverageState', - 5: 'Refurbished', - 6: 'New', - Price *(float)* - EcoPart *(float)* - Vat *(float)* - DeaTax *(float)* - Stock *(int)* - PreparationTime *(int)* - Optional attributes: - Comment *(str)* - StrikedPrice *(float)* - PriceMustBeAligned *(str)*: - 'Empty', - 'Unknown', - 'Align', - 'DontAlign', - MinimumPriceForPriceAlignment *(float)* - ProductPackagingUnit *(str)*: - 'None', - 'Liter', - 'Kilogram', - 'SquareMeter', - 'CubicMeter' - ProductPackagingValue *(float)* - BluffDeliveryMax *(int)* -ShippingInformation: - AdditionalShippingCharges *(float)* - DeliveryMode *(DeliveryModeInformation)* - 'STD' ('Standart') - 'TRK' ('Tracking') - 'REG' ('Registered') - 'COL' ('Collissimo') - 'RCO' ('Relay Colis') - 'REL' ('Mondial Relay') - 'SO1' ('So Colissimo') - 'MAG' ('in shop') - 'LV1' - 'LV2' - 'LV' - 'FST' - 'EXP' - 'RIM' - ShippingCharges *(float)* Example:: response = api.offers.generate_offer_package( package_name, package_path, offers_list, offer_publication_list=offer_publication_list, purge_and_replace=purge_and_replace ) :returns: None """ return generate_package( "offer", package_path, { "OfferCollection": offers_list, "OfferPublicationList": offer_publication_list, "PurgeAndReplace": purge_and_replace, "Name": package_name, "PackageType": package_type }, overwrite=overwrite ) @auto_refresh_token def submit_offer_package(self, url): """ To import offers. It is used to add new offers to the Cdiscount marketplace or to modify/update offers that already exists. .. note:: The wanted zip package could be generate by calling ``api.offers.generate_offer_package(offer_list)`` Then you'll have to get an url to download zip package Finally, you can use submit_offer_package(url) Examples:: api.offers.submit_offer_package(url) :return: the id of package or -1 """ offer_package = self.api.factory.OfferPackageRequest(url) # Send request. response = self.api.client.service.SubmitOfferPackage( headerMessage=self.api.header, offerPackageRequest=offer_package ) return serialize_object(response, dict) @auto_refresh_token def get_offer_package_submission_result(self, package_id): """ This operation makes it possible to know the progress report of the offers import. :param long package_id: id of package we want to know the progress :return: Offer report logs """ package = self.api.factory.PackageFilter(package_id) response = self.api.client.service.GetOfferPackageSubmissionResult( headerMessage=self.api.header, offerPackageFilter=package ) return serialize_object(response, dict) PK\&O'%n1CCcdiscountapi/sections/orders.py# -*- coding: utf-8 -*- """ cdiscountapi.sections.orders ---------------------------- Handles the orders. :copyright: © 2019 Alexandria """ from zeep.helpers import serialize_object from cdiscountapi.helpers import check_element, get_motive_id from .base import BaseSection from ..helpers import auto_refresh_token def update_date_filter(date_filter, p1, p2): """ Make sure the filter on a date with param p1 is always the same as param p2 if p2 is not provided. Example: new_filter = update_date_filter( { 'BeginCreationDate': datetime.datetime(2019, 9, 6)}, 'BeginCreationDate', 'BeginModificationDate' ) new_filter is: { 'BeginCreationDate': datetime.datetime(2019, 9, 6), 'BeginModificationDate': datetime.datetime(2019, 9, 6), } """ # If the first parameter is not in the filter, we just return the original filter if p1 not in date_filter: return date_filter new_date_filter = date_filter.copy() if p1 in date_filter and p2 not in new_date_filter: new_date_filter.update({p2: new_date_filter[p1]}) return new_date_filter def complete_date_filter(date_filter): """ Update the date filter with the missing date parameters If BeginCreationDate is present but not BeginModificationDate, add BeginModificationDate (and conversely) If EndCreationDate is present but not EndModificationDate, add EndModificationDate (and conversely) """ date_filter = update_date_filter( date_filter, "BeginCreationDate", "BeginModificationDate" ) date_filter = update_date_filter( date_filter, "BeginModificationDate", "BeginCreationDate" ) date_filter = update_date_filter( date_filter, "EndCreationDate", "EndModificationDate" ) date_filter = update_date_filter( date_filter, "EndModificationDate", "EndCreationDate" ) return date_filter class Orders(BaseSection): """ Allows to list, validate or refund orders. Methods:: get_order_list(**order_filter) get_global_configuration() prepare_validations(data) validate_order_list(**validate_order_list_message) create_refund_voucher(**request) manage_parcel(parcel_actions_list=parcel_actions_list, scopus_id=scopus_id) Operations are included in the Orders API section. (https://dev.cdiscount.com/marketplace/?page_id=128) """ @auto_refresh_token def get_order_list(self, **order_filter): """ To search orders. This operation makes it possible to seek orders according to the following criteria: Example:: response = api.orders.get_order_list() - The order state: - CancelledByCustomer - WaitingForSellerAcceptation - AcceptedBySeller - PaymentInProgress - WaitingForShipmentAcceptation - Shipped - RefusedBySeller - AutomaticCancellation (ex: no answer from the seller) - PaymentRefused - ShipmentRefusedBySeller - Waiting for Fianet validation "A valider Fianet" (None) - Validated Fianet - RefusedNoShipment - AvailableOnStore - NonPickedUpByCustomer - PickedUp - Filled Example:: response = api.orders.get_order_list( States=['CancelledByCustomer', 'Shipped'] ) - Recovery or not of the products of the order - Filter on date: - BeginCreationDate - EndCreationDate - BeginModificationDate - EndModificationDate Example:: response = api.orders.get_order_list( BeginCreationDate=datetime.datetime(2077, 1, 1), States=['CancelledByCustomer', 'Shipped'] ) .. note:: The date parameters must be combined with another parameter (like ``States``). Otherwise all the orders will be taken into account. - Order number list Liste (OrderReferenceList) Warning, this filter cannot be combined with others. If there is an order list, the other filters are unaccounted. Example:: response = api.orders.get_order_list(OrderReferenceList=['X1', 'X2']) - Filter on website thanks to the corporationCode. Example:: response = api.orders.get_order_list(CorporationCode='CDSB2C') - Filter by Order Type: - MKPFBC Orders (Marketplace fulfillment by Cdiscount) - EXTFBC Orders (External fulfillment by Cdiscount) - FBC Orders (Isfulfillment) - None Example:: response = api.get_order_list(OrderType=None) - PartnerOrderRef filter from 1 to N external order (if it's a multiple search, separate PartnerOrderRefs by semicolon). PartnerOrderRef is the seller's reference Example:: response = api.get_order_list(PartnerOrderRef='SELLER_REF') - Recovery or not of the parcels of the order Example:: response = api.get_order_list(FetchParcels=True, OrderReferenceList=['X1']) .. note:: If ``FetchOrderLines`` is not specified in keywords, its default value will be ``True``. """ if "States" in order_filter: order_filter.update( States=self.api.factory.ArrayOfOrderStateEnum(order_filter["States"]) ) # For some reasons, when a date parameter is used, its "pair" must be used too # Example: BeginCreationDate and BeginModificationDate must be used together # (idem for EndCreationDate and EndModificationDate) # We address this weird behavior with `complete_date_filter`. order_filter = complete_date_filter(order_filter) order_filter = self.update_with_valid_array_type( order_filter, {"OrderReferenceList": "string"} ) if "FetchOrderLines" not in order_filter: order_filter.update(FetchOrderLines=True) order_filter = self.api.factory.OrderFilter(**order_filter) response = self.api.client.service.GetOrderList( headerMessage=self.api.header, orderFilter=order_filter ) return serialize_object(response, dict) @auto_refresh_token def get_global_configuration(self): """ Get cdiscount settings. This method allows to get a list of several settings: - Carrier list """ response = self.api.client.service.GetGlobalConfiguration( headerMessage=self.api.header ) return serialize_object(response, dict) def _prepare_validation(self, data): """ Return the validation data for an order. :param dict data: The information about the order to validate. (cf `Seller.prepare_validations`) """ data = data.copy() # Check elements in ValidateOrder for element_name in data.keys(): check_element(element_name, self.api.factory.ValidateOrder) # check elements ValidateOrderLine for i, validate_order_line in enumerate(data["OrderLineList"]): for element_name in validate_order_line.keys(): check_element(element_name, self.api.factory.ValidateOrderLine) data["OrderLineList"] = { "ValidateOrderLine": [x for x in data.pop("OrderLineList")] } return serialize_object(self.api.factory.ValidateOrder(**data), dict) def prepare_validations(self, data): """ Return the dictionary used to validate the orders in :py:meth:`Orders.validate_order_list` This method tries to simplify the creation of the data necessary to validate the orders by letting the user provide a more intuitive data structure than the one required for the request. :param list data: The validation data for the orders. A list of dictionaries with the following structure: .. code-block:: python { 'CarrierName': carrier_name, 'OrderNumber': order_number, 'OrderState': order_state, 'TrackingNumber': tracking_number, 'TrackingUrl': tracking_url, 'OrderLineList': [ { 'AcceptationState': acceptation_state, 'ProductCondition': product_condition, 'SellerProductId': seller_product_id, 'TypeOfReturn': type_of_return }, ... ] } :returns: The ``validate_order_list_message`` dictionary created with ``data`` """ return { "OrderList": {"ValidateOrder": [self._prepare_validation(x) for x in data]} } # TODO Use for accept_orders @auto_refresh_token def validate_order_list(self, **validate_order_list_message): """ Validate a list of orders :param validate_order_list_message: The information about the orders to validate. There are two ways to create a ``validate_order_list_message``: 1. you can build the dictionary by yourself: Example:: response = api.validate_order_list( OrderList= {'ValidateOrder': [{'CarrierName': carrier_name, 'OrderNumber': order_number, 'OrderState': order_state, 'TrackingNumber': tracking_number, 'TrackingUrl': tracking_url, 'OrderLineList': { 'ValidateOrderLine': [ {'AcceptationState': 'acceptation_state', 'ProductCondition': product_condition, 'SellerProductId': seller_product_id, 'TypeOfReturn': type_of_return}, ... ]}, }, ... ]}) 2. you can use :py:meth:`Orders.prepare_validations`: Example:: validate_order_list_message = api.orders.prepare_validations( [{'CarrierName': carrier_name, 'OrderNumber': order_number, 'OrderState': order_state, 'TrackingNumber': tracking_number, 'TrackingUrl': tracking_url, 'OrderLineList': [ {'AcceptationState': 'acceptation_state', 'ProductCondition': product_condition, 'SellerProductId': seller_product_id, 'TypeOfReturn': type_of_return}, ... ]}, ...] ) response = api.orders.validate_order_list_message(**validate_order_list_message) """ validate_order_list_message = self.api.factory.ValidateOrderListMessage( **validate_order_list_message ) response = self.api.client.service.ValidateOrderList( headerMessage=self.api.header, validateOrderListMessage=validate_order_list_message, ) return serialize_object(response, dict) @auto_refresh_token def create_refund_voucher(self, **request): """ This method still allows refunding lines of an order whose state is "ShippedBySeller". An additional feature allows to make a commercial gesture on an order MKPCDS before and after shipping and on an order MKPFBC after shipping. :param list CommercialGestureList: - Amount *(decimal)* - Sku *(str)*: The product number - MotiveId *(int)*: - 131: 'Compensation on missing stock', - 132: 'Product / Accessory delivered damaged or missing', - 133: 'Error of reference, color, size', - 134: 'Fees unduly charged to the customer', - 135: 'Late delivery', - 136: 'Product return fees', - 137: 'Shipping fees', - 138: 'Warranty period or rights of with drawal passed', - 139: 'Others' :param str OrderNumber: :param list SellerRefundList: - Mode *(str)*: - 'Claim' - 'Retraction' - Motive *(str)*: - 'VendorRejection', - 'ClientCancellation', - 'VendorRejectionAndClientCancellation', - 'ClientClaim', - 'VendorInitiatedRefund', - 'ClientRetraction', - 'NoClientWithDrawal', - 'ProductStockUnavailable' - SellerRefundOrderLine: - EAN *(str)* - RefundShippingChanges *(bool)* - SellerProductId *(str)* Example:: response = api.orders.create_refund_voucher( CommercialGestureList=[ { 'Amount': 10, 'MotiveId': 135 } ], OrderNumber='ORDER_NUMBER_1', SellerRefundList={ 'Mode': 'Claim', 'Motive': 'ClientClaim', 'RefundOrderLine': { 'Ean': '4005274238223', 'RefundShippingCharges': True, 'SellerProductId': '42382235' } } ) """ # Check CommercialGestureList if "CommercialGestureList" in request: commercial_gestures = request["CommercialGestureList"] if isinstance(commercial_gestures, dict): commercial_gestures = [commercial_gestures] for commercial_gesture in commercial_gestures: motive_id = commercial_gesture.get("MotiveId") # MotiveId is a label # if not isinstance(motive_id, int): # motive_id = get_motive_id(motive_id) # TODO Damien: voir pour obligation d'int if isinstance(motive_id, int): commercial_gesture["MotiveId"] = motive_id else: commercial_gestures = None # Request request = self.api.factory.CreateRefundVoucherRequest( OrderNumber=request.get("OrderNumber"), CommercialGestureList=self.api.factory.ArrayOfRefundInformation( commercial_gestures ), SellerRefundList=self.api.factory.ArrayOfSellerRefundRequest( request.get("SellerRefundList") ), ) response = self.api.client.service.CreateRefundVoucher( headerMessage=self.api.header, request=request ) return serialize_object(response, dict) @auto_refresh_token def manage_parcel(self, parcel_actions_list=None, scopus_id=None): """ Ask for investigation or ask for delivery certification. :param list parcel_actions_list: The list of dictionaries with the keys: - ManageParcel: ('AskingForDeliveryCertification' or 'AskingForInvestigation') - ParcelNumber: The parcel customer number - Sku: The product number :param int scopus_id: The scopus id Usage:: api.manage_parcel(parcel_actions_list=[ {'ManageParcel': manage_parcel, 'ParcelNumber': parcel_number, 'Sku': sku}, ... ], scopus_id=scopus_id ) """ # Handle properly the case where no information is provided for # parcel_actions_list if parcel_actions_list is not None: new_parcel_actions_list = [] for parcel_infos in parcel_actions_list: new_parcel_infos = self.api.factory.ParcelInfos(**parcel_infos) if not isinstance(new_parcel_infos.ManageParcel, list): new_parcel_infos.ManageParcel = [new_parcel_infos.ManageParcel] new_parcel_actions_list.append(new_parcel_infos) else: new_parcel_actions_list = None manage_parcel_request = self.api.factory.ManageParcelRequest( ParcelActionsList=self.api.factory.ArrayOfParcelInfos( new_parcel_actions_list ), ScopusId=scopus_id, ) response = self.api.client.service.ManageParcel( headerMessage=self.api.header, manageParcelRequest=manage_parcel_request ) return serialize_object(response, dict) PKyOKŁ##!cdiscountapi/sections/products.py# -*- coding: utf-8 -*- """ cdiscountapi.sections.products ------------------------------ Handles the products. :copyright: © 2019 Alexandria """ from tempfile import gettempdir from zeep.helpers import serialize_object from cdiscountapi.helpers import generate_package from .base import BaseSection from ..helpers import auto_refresh_token class Products(BaseSection): """ Allows to get information about products and submit new products on Cdiscount. Methods:: get_all_allowed_category_tree() get_allowed_category_tree() get_product_list(category_code) get_model_list(category=category) get_all_model_list() get_brand_list() generate_product_package(package_name, products_list) submit_product_package(url) get_product_package_submission_result(package_ids=package_ids) get_product_package_product_matching_file_data(package_id) get_product_list_by_identifier(ean_list=ean_list) Operations are included in the Products API section. (https://dev.cdiscount.com/marketplace/?page_id=220) """ @auto_refresh_token def get_allowed_category_tree(self): """ Categories which are accessible to the seller. Usage:: response = api.products.get_allowed_category_tree() :return: tree of the categories leaves of which are authorized for the integration of products and/or offers """ response = self.api.client.service.GetAllowedCategoryTree( headerMessage=self.api.header ) return serialize_object(response, dict) @auto_refresh_token def get_all_allowed_category_tree(self): """ All categories. Usage:: response = api.products.get_all_allowed_category_tree() :return: tree of the categories leaves of which are authorized for the integration of products and/or offers """ from ..cdiscountapi import Connection api_all = Connection("AllData", "pa$$word", header_message=self.header) response = api_all.client.service.GetAllAllowedCategoryTree( headerMessage=api_all.header ) return serialize_object(response, dict) @auto_refresh_token def get_product_list(self, category_code): """ Search products in the reference frame. :param str category_code: code to filter products by category Usage:: response = api.products.get_product_list("13380D0501") :return: products corresponding to research """ filters = self.api.factory.ProductFilter(category_code) response = self.api.client.service.GetProductList( headerMessage=self.api.header, productFilter=filters ) return serialize_object(response, dict) @auto_refresh_token def get_model_list(self, category=None): """ Model categories allocated to the seller. :param str category: category code to filter results Usages:: response = api.products.get_model_list() response = api.products.get_model_list("13380D0501") :return: models and mandatory model properties """ categories = category if isinstance(category, (list, tuple)) else [category] model_filter = self.api.factory.ModelFilter( self.array_of('string', categories) ) response = self.api.client.service.GetModelList( headerMessage=self.api.header, modelFilter=model_filter ) return serialize_object(response, dict) # TODO find a way to call it. @auto_refresh_token def get_all_model_list(self): """ .. warning:: Doesn't work at the moment. Model categories opened on marketplace. Usage:: response = api.products.get_all_model_list() :return: models and mandatory model properties """ # api_all = Connection('AllData', 'pa$$word') # response = api_all.client.service.GetAllModelList( # headerMessage=api_all.header, # ) # return serialize_object(response, dict) pass @auto_refresh_token def get_brand_list(self): """ Complete list of the brands Usage:: response = api.products.get_brand_list() :return: all brands """ response = self.api.client.service.GetBrandList(headerMessage=self.api.header) return serialize_object(response, dict) @staticmethod def generate_product_package(package_name, products_list, overwrite=True): """ Generate a zip product package as cdiscount wanted. :param str package_name: [mandatory] the full path to the offer package (without .zip) :param bool overwrite: [optional] Determine if an existing package is overwritten when a new one with the same name is created (default: True) :param list products_list: - Mandatory attributes: - BrandName *(str)* - Description *(str)* - LongLabel *(str)* - Model *(str)* - Navigation *(str)* - ProductKind *(str)* - 'Variant' - 'Standart' - SellerProductId *(str)* - ShortLabel *(str)* - Optional attributes: - Width *(int)* - Weight *(int)* - Length *(int)* - Height *(int)* - Size *(str)* - SellerProductFamily *(str)* - SellerProductColorName *(str)* - ManufacturerPartNumber *(str)* - ISBN *(str)* - EncodedMarketingDescription *(str)* Example:: response = api.products.generate_product_package(products_list) """ return generate_package( "product", package_name, {"Products": products_list}, overwrite=overwrite ) @auto_refresh_token def submit_product_package(self, url): """ To ask for the creation of products. It could included between 10K and 20K products by package. There is 2 ways to use it. 1. You can generate a zip package with: api.products.generate_product_package(products_dict) 2. You can generate the package yourself before uploading. Then you'll have to get an url to download zip package Finally, you can use submit_product_package(url) Examples:: api.products.submit_product_package(url) :return: the id of package or -1 """ product_package = self.api.factory.ProductPackageRequest(url) # Send request. response = self.api.client.service.SubmitProductPackage( headerMessage=self.api.header, productPackageRequest=product_package ) return serialize_object(response, dict) # TODO find why it doesn't work. @auto_refresh_token def get_product_package_submission_result(self, package_ids=None): """ Progress status of a product import. :param long package_ids: PackageID Usage:: response = api.products.get_product_package_submission_result(2154894) :return: partial or complete report of package integration """ filters = self.api.factory.PackageFilter(package_ids) response = self.api.client.service.GetProductPackageSubmissionResult( headerMessage=self.api.header, productPackageFilter=filters ) return serialize_object(response, dict) @auto_refresh_token def get_product_package_product_matching_file_data(self, package_id): """ Information of the created products. :param long package_id: package id to filter results Usage:: response = api.products.get_product_package_product_matching_file_data(21454894) :return: information of the created products """ if package_id: response = self.api.client.service.GetProductPackageProductMatchingFileData( headerMessage=self.api.header, productPackageFilter={"PackageID": package_id}, ) return serialize_object(response, dict) @auto_refresh_token def get_product_list_by_identifier(self, ean_list=[]): """ Obtain details for a list of products :param list ean_list: list of EAN to filter Usage:: response = api.products.get_product_list_by_identifier('2009863600561') :return: complete list of products """ request = {"IdentifierType": "EAN", "ValueList": ean_list} response = self.api.client.service.GetProductListByIdentifier( headerMessage=self.api.header, identifierRequest=request ) return serialize_object(response, dict) PKNN cdiscountapi/sections/relays.py# -*- coding: utf-8 -*- """ cdiscountapi.sections.relays ---------------------------- Handles the relays. :copyright: © 2019 Alexandria """ from zeep.helpers import serialize_object from .base import BaseSection from ..helpers import auto_refresh_token class Relays(BaseSection): """ Allows to get information about the different available relays and submit new ones. Methods:: get_parcel_shop_list() submit_relays_file(relays_file_uri) get_relays_file_submission_result(relays_file_ids) Operations are included in the Relays API section. (https://dev.cdiscount.com/marketplace/?page_id=108) """ @auto_refresh_token def get_parcel_shop_list(self): """ To get a list of relays known. Usage:: response = api.relays.get_parcel_shop_list() """ response = self.api.client.service.GetParcelShopList( headerMessage=self.api.header ) return serialize_object(response, dict) @auto_refresh_token def submit_relays_file(self, relays_file_uri): """ Send information about relays in a file :param str relays_file_uri: A link pointing to a XLSX file with information about relays Usage:: response = api.relays.submit_relays_file( 'http://spreadsheetpage.com/downloads/xl/worksheet%20functions.xlsx' ) where relays_file_uri is the URI to a XLSX file :returns: The response with the RelaysFileId for the file. """ relays_file_request = self.api.factory.RelaysFileIntegrationRequest( relays_file_uri ) response = self.api.client.service.SubmitRelaysFile( headerMessage=self.api.header, relaysFileRequest=relays_file_request ) return serialize_object(response, dict) @auto_refresh_token def get_relays_file_submission_result(self, relays_file_ids): """ Get the state of progress of the relays file submission. :param list relays_file_ids: IDs referencing the relays file submitted. Usage:: response = api.get_relays_file_submission_result([15645,52486]) where ``relays_file_id`` is the value of RelaysFileId returned by `SubmitRelaysFile `_. :returns: The response with the information about the integration of the specified relays. """ relays_file_filter = self.api.factory.RelaysFileFilter(relays_file_ids) response = self.api.client.service.GetRelaysFileSubmissionResult( headerMessage=self.api.header, relaysFileFilter=relays_file_filter ) return serialize_object(response, dict) PKNN>u cdiscountapi/sections/sandbox.py# -*- coding: utf-8 -*- """ cdiscountapi.sections.sandbox ----------------------------- Simulates the different processes involved in an order. :copyright: © 2019 Alexandria """ from zeep.helpers import serialize_object from .base import BaseSection class Sandbox(BaseSection): """ Allows create fake orders, simulate payments, simulate cancellations, get, validate fake orders and create refund vouchers after the shipment. Operations are included in the Sandbox API section. (https://dev.cdiscount.com/marketplace/?page_id=2224) """ def create_fake_order(self, **request): """ Create a fake order Usage:: response = api.sandbox.create_fake_order(request={ 'NumberOfProducts': 1, ProductType: 'Variant', 'PaymentMode': 'CB1X', 'Quantity': 1, 'ShippingCode': 'Normal', 'Tenant': 'CDiscount'}) """ response = self.api.factory. PKNN^"cdiscountapi/sections/seller.py# -*- coding: utf-8 -*- """ cdiscountapi.sections.seller ---------------------------- Handles the seller information. :copyright: © 2019 Alexandria """ from zeep.helpers import serialize_object from .base import BaseSection from ..helpers import auto_refresh_token class Seller(BaseSection): """ Seller section lets sellers retrieve information about their seller account and their performance indicator. Methods:: get_seller_info() get_seller_indicators() Operations are included in the Seller API section. (https://dev.cdiscount.com/marketplace/?page_id=36) """ @auto_refresh_token def get_seller_info(self): """ To get seller info as: - Delivery Modes - Offer Pool - Email - Login - Phone Number - Adress - Name - Relays - Shop Name/Url - SIRET - Seller Availability - Account State :return: Information of the authenticated seller. """ response = self.api.client.service.GetSellerInformation( headerMessage=self.api.header ) return serialize_object(response, dict) @auto_refresh_token def get_seller_indicators(self): """ To get all rates about seller as: - Order Acceptation - Product Shipping - Order with claim - Order with refund - Preparation Deadline respected :return: a dict with the data of the user """ response = self.api.client.service.GetSellerIndicators( headerMessage=self.api.header ) return serialize_object(response, dict) PKNN|++ cdiscountapi/sections/webmail.py# -*- coding: utf-8 -*- """ cdiscountapi.sections.webmail ----------------------------- Handles the email addresses of the customers. :copyright: © 2019 Alexandria """ from zeep.helpers import serialize_object from .base import BaseSection from ..helpers import auto_refresh_token class WebMail(BaseSection): """ The WebMail API allows the seller to retrieve encrypted email address to contact a customer Methods:: generate_discussion_mail_guid(order_id) get_discussion_mail_list(discussion_ids) Operations are included in the WebMail API section. (https://dev.cdiscount.com/marketplace/?page_id=167) """ @auto_refresh_token def generate_discussion_mail_guid(self, order_id): """ Generate an encrypted mail address from an order. This operation allows getting an encrypted mail address to contact a customer. :param str order_id: Order id for which an e-mail is to be sent. Usage:: response = api.generate_discussion_mail_guid(order_id) """ response = self.api.client.service.GenerateDiscussionMailGuid( headerMessage=self.api.header, request={"ScopusId": order_id} ) return serialize_object(response, dict) @auto_refresh_token def get_discussion_mail_list(self, discussion_ids): """ Get encrypted mail addresses from discussions. This operation allows getting an encrypted mail address to contact a customer. Usages:: id = 113163877 response = api.webmail.get_discussion_mail_list(id) ids = [113163877, 224274988] response = api.webmail.get_discussion_mail_list(ids) """ request = self.api.factory.GetDiscussionMailListRequest( DiscussionIds=self.array_of("long", discussion_ids) ) response = self.api.client.service.GetDiscussionMailList( headerMessage=self.api.header, request=request ) return serialize_object(response, dict) PKyO!cdiscountapi/templates/Offers.xml {% for offer in offers %} {% for shipping_info in offer['shipping_information_list'] %} {% endfor %} {% if 'discount_list' in offer %} {% for discount_component in offer['discount_list'] %} {% endfor %} {% endif %} {% endfor %} {% if offer_publication_list %} {% for publication_pool in offer_publication_list %} {% endfor %} {% endif %} PKNN8a#cdiscountapi/templates/Products.xml {% for product in products %} {% for product_ean in product['EanList'] %} {% endfor %} {% for model_property in product['ModelProperties'] %} {% for key, value in model_property.items() %} {{ value }} {% endfor %} {% endfor %} {% for product_image in product['Pictures'] %} {% endfor %} {% endfor %} PKNN' =JJ$cdiscountapi-0.1.4.dist-info/LICENSEThe MIT License (MIT) Copyright (c) 2019 ZIBOURA Mathilde, RABOIS Damien Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HPO"cdiscountapi-0.1.4.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,szd&Y)r$[)T&UrPK!H%cdiscountapi-0.1.4.dist-info/METADATAn1)Ff"-2b"9( A=eO1&Tɥ̌1 %?ڶ X?H]_<[t} Ρ) W_at>X?dehmx(X[_rz;/)Guika4;.G#tSQ^/m;ydw>N{ lK6[>dU#Bw( .*(v*էG_kbb|}{In\qg#6g~=Iiݘ&Y_*k9|thujM83}H8l+cPpn7(+_nes,#4p k6w'uN4S vFlxj.WaF9'ØPK!HfP #cdiscountapi-0.1.4.dist-info/RECORDYF|tؗ< (JQ " |x;dLRPE?$QRU7[UEѼc(Iy(}Ԩ8ojqOtO_|T8XA?%͓e ^ ,p; euv`IP-G h) :hBp219)|!^">BmM/b,E}3>sA0SrG2]M'ڗYR} @Goۼ">9ݥ9Y5^[,rҟ1%1U*!СNy~KJ6khE6[F .wf5T,%[N_P]"b+Cj:T32yp#\ #}1ibJ'nJFcmWI !x:ވX+[A^Z 2~+<~[8UK X9|oBޝ;G ̠QxϻGC ~uU#܁qIsӉFif‚Kek x@M|[q>" qR2)|L-%qVGTY3Q.t6)A~91珕nWmKV<{ u# F8 AD:A.ݴUn k~C)JV {ڰ5iwut {`8,:⻝C?H;^"Q#5<RFe:Y2!"m2PKyOcdiscountapi/__init__.pyPK=bN%cdiscountapi/cdiscountapi.pyPKNNَAyy큃cdiscountapi/exceptions.pyPKce2OL$$4cdiscountapi/helpers.pyPKYNj;cdiscountapi/.tox/.package.lockPKNNHw;cdiscountapi/assets/config.yamlPKNNvt<cdiscountapi/assets/offer.jsonPKyO""!Pcdiscountapi/packages/__init__.pyPKNNd(`  "scdiscountapi/packages/validator.pyPKNNڸPN7}cdiscountapi/packages/offer_package/[Content_Types].xmlPKNN6Kcdiscountapi/packages/offer_package/Content/Offers.xmlPKzOzQg/cdiscountapi/packages/offer_package/_rels/.relsPKNNpWNN9ـcdiscountapi/packages/product_package/[Content_Types].xmlPKNN:~cdiscountapi/packages/product_package/Content/Products.xmlPKNNGj>1ւcdiscountapi/packages/product_package/_rels/.relsPKNN^!cdiscountapi/sections/__init__.pyPKNNsPZD||cdiscountapi/sections/base.pyPKNNQQYY$cdiscountapi/sections/discussions.pyPKNNm55$Lcdiscountapi/sections/fulfillment.pyPK*{"O`W!!{cdiscountapi/sections/offers.pyPK\&O'%n1CCccdiscountapi/sections/orders.pyPKyOKŁ##!*=cdiscountapi/sections/products.pyPKNN `cdiscountapi/sections/relays.pyPKNN>u lcdiscountapi/sections/sandbox.pyPKNN^"pcdiscountapi/sections/seller.pyPKNN|++ Iwcdiscountapi/sections/webmail.pyPKyO!cdiscountapi/templates/Offers.xmlPKNN8a#χcdiscountapi/templates/Products.xmlPKNN' =JJ$cdiscountapi-0.1.4.dist-info/LICENSEPK!HPO"Lcdiscountapi-0.1.4.dist-info/WHEELPK!H%ܑcdiscountapi-0.1.4.dist-info/METADATAPK!HfP #Ɠcdiscountapi-0.1.4.dist-info/RECORDPK 9