class CheckCategories: """ Class responsible for checking if the categories exist on WooCommerce """ def __init__(self, woocommerce_records: List[Dict]): self._woocommerce_records = woocommerce_records self._api: Optional[WooCommerceAPI] = None self._categories: Dict = dict() self._missing_categories: List = [] def api_setup(self): """ Setup WooCommerce API object """ self._api = WooCommerceAPI( username=config.WooCommerceAPICred.USERNAME, password=config.WooCommerceAPICred.PASSWORD, ) def get_category_id(self, category: str): """ From the category name get the category id """ # Category has hierarchy information cat_list = [x.strip() for x in category.split(">")] parents = [] # List of dict [{"id": 123}. {"id": 456}] for name in cat_list: if parents: woocommerce_categories, _ = self._api.get_all_categories( search=name, parent=parents[-1]["id"]) else: woocommerce_categories, _ = self._api.get_all_categories( search=name) if not woocommerce_categories: print(f"No woocommerce categories found for name {name}") self._missing_categories.append(name) return parents[-1] filtered_category = self.filter_category(woocommerce_categories, name) parents.append({"id": filtered_category["id"]}) return parents[-1] def find_missing_category(self): """ Find missing categories """ self.api_setup() for record in self._woocommerce_records: category = record["Categories"] if category is not None: list_of_categories = [x.strip() for x in category.split(",")] for catname in list_of_categories: if catname not in self._categories: self._categories[catname] = self.get_category_id( catname) @staticmethod def filter_category(category_list: List, category: str): """ Filter out the category based on category name. It will only return one category """ for cat in category_list: if cat["name"] == category: return cat return category_list.pop() @property def missing_categories(self): return self._missing_categories
def api_setup(self): """ Setup WooCommerce API object """ self._api = WooCommerceAPI( username=config.WooCommerceAPICred.USERNAME, password=config.WooCommerceAPICred.PASSWORD )
class ProductIntegration: """ Product Integration with WooCommerce website using API """ class UpdateImageCode(IntEnum): ALlProducts = 1 NewProducts = 2 # Update for new products or for products that don't have images PRODUCT_LIMIT = os.environ.get("PRODUCT_LIMIT") or 20 UPDATE_IMAGE_MODE = UpdateImageCode.ALlProducts def __init__( self, csv_file: str, template: str, update_image_mode: UpdateImageCode = UpdateImageCode.ALlProducts ): self._csv_file: str = csv_file self._template: str = template self._update_image_mode: ProductIntegration.UpdateImageCode = update_image_mode self._api: Optional[WooCommerceAPI] = None self._supplier_csv_2_woocommerce_csv: Optional[SupplierCSV2WoocommerceCSV] = None self._map_csv_to_api: Optional[MapCsvToApi] = None self._api_data: Optional[List] = None self._woocommerce_products: Optional[List] = None self._api_responses: List = [] self._products_upload_table: List[Dict] = [] def api_setup(self): """ Setup WooCommerce API object """ self._api = WooCommerceAPI( username=config.WooCommerceAPICred.USERNAME, password=config.WooCommerceAPICred.PASSWORD ) def setup(self): """ Setup """ print(f"Running in update image mode: {self._update_image_mode}") self.api_setup() self._supplier_csv_2_woocommerce_csv = SupplierCSV2WoocommerceCSV( csv_file=self._csv_file, template=self._template ) self._supplier_csv_2_woocommerce_csv.convert() self._map_csv_to_api = MapCsvToApi( csv_data=self._supplier_csv_2_woocommerce_csv.product_records ) self._map_csv_to_api.map() self._api_data = self._map_csv_to_api.api_data skus = [x[ApiProductFields.Sku] for x in self._api_data] # Get all the products available on WooCommerce self._woocommerce_products, _ = self._api.get_all_products_as_dict(skus=skus) def create_or_update_products(self): """ Create new products or update existing products """ print(f"Using product limit of {ProductIntegration.PRODUCT_LIMIT} for batch request") create_product_data: List = [] update_product_data: List = [] for product in self._api_data: product_sku = product.get(ApiProductFields.Sku) if product_sku is None: print(f"Skipping processing product without {ApiProductFields.Sku}") continue # IF the products exist and the product has images, then don't update the image in # NewProducts mode. In all other cases update the images. if product_sku in self._woocommerce_products: print(f"Updating product for SKU {product_sku}") if self._update_image_mode == ProductIntegration.UpdateImageCode.NewProducts: # Image already exist. So we shouldn't update the image in this mode if self._woocommerce_products[product_sku].get(ApiProductFields.Images) and \ product.get(ApiProductFields.Images): # Remove "images" key from the product print(f"Removing images for SKU {product_sku}") product.pop(ApiProductFields.Images) product_id = self._woocommerce_products[product_sku][ApiProductFields.Id] product.update({ApiProductFields.Id: product_id}) update_product_data.append(product) else: print(f"Creating product for SKU {product_sku}") create_product_data.append(product) # Create a table for the status of products self._products_upload_table += [ {"SKU": product.get(ApiProductFields.Sku), "Status": "Created"} for product in create_product_data ] self._products_upload_table += [ {"SKU": product.get(ApiProductFields.Sku), "Status": "Updated"} for product in update_product_data ] # Max of 100 objects can be created or updated if len(create_product_data) + len(update_product_data) > ProductIntegration.PRODUCT_LIMIT: # Send create and update request separately # Send create request in a batch of 100 objects while create_product_data: response, success = self._api.create_multiple_products( data=create_product_data[:ProductIntegration.PRODUCT_LIMIT] ) # Save the response if success is False if not success: self._api_responses.append(response) create_product_data = create_product_data[ProductIntegration.PRODUCT_LIMIT:] # Send update request in a batch of 100 objects while update_product_data: response, success = self._api.update_multiple_products( data=update_product_data[:ProductIntegration.PRODUCT_LIMIT] ) # Save the response if success is False if not success: self._api_responses.append(response) update_product_data = update_product_data[ProductIntegration.PRODUCT_LIMIT:] else: response, success = self._api.create_or_update_products( create_data=create_product_data, update_data=update_product_data ) # Save the response if success is False if not success: self._api_responses.append(response) @property def api_data(self): return self._api_data @property def api_responses(self): return self._api_responses @property def product_upload_table(self): return self._products_upload_table
class MapCsvToApi: """ Class responsible for mapping CSV data to API data """ def __init__(self, csv_data: List): self._csv_data: List = csv_data self._api_data: List = [] self._api: Optional[WooCommerceAPI] = None self._categories: Dict = dict() def api_setup(self): """ Setup WooCommerce API object """ self._api = WooCommerceAPI( username=config.WooCommerceAPICred.USERNAME, password=config.WooCommerceAPICred.PASSWORD, ) def get_category_id(self, category: str): """ From the category name get the category id """ # Category has hierarchy information cat_list = [x.strip() for x in category.split(">")] parents = [] # List of dict [{"id": 123}. {"id": 456}] for name in cat_list: if parents: woocommerce_categories, _ = self._api.get_all_categories( search=name, parent=parents[-1]["id"]) else: woocommerce_categories, _ = self._api.get_all_categories( search=name) if not woocommerce_categories: print(f"No woocommerce categories found for name {name}") filtered_category = self.filter_category(woocommerce_categories, name) parents.append({"id": filtered_category["id"]}) return parents[-1] def map_csv_category(self, category: str): """ Map categories from csv to woocommerce api """ list_of_categories = [x.strip() for x in category.split(",")] list_of_categories_id = [] for catname in list_of_categories: if catname not in self._categories: self._categories[catname] = self.get_category_id(catname) list_of_categories_id.append(self._categories[catname]) return list_of_categories_id def csv_category_to_api(self): """ Replace the CSV category with API category """ # [{'id': 49}, {'id': 60}, {'id': 146}, {'id': 1882}] for data in self._api_data: data["categories"] = self.map_csv_category(data["categories"]) # data["categories"] = [{'id': 49}, {'id': 60}, {'id': 146}, {'id': 1882}] def map_csv_attributes(self, attributes: List[tuple]): """ Map all the attributes from CSV to API data """ woocommerce_attributes, _ = self._api.get_all_attributes() api_attributes = [] for index, attrs in enumerate(attributes): woo_attr = next( (x for x in woocommerce_attributes if x["name"] == attrs[0]), None) woo_attr_id = 0 woo_attr_name = attrs[0] if woo_attr: woo_attr_id = woo_attr["id"] woo_attr_name = woo_attr["name"] api_attributes.append({ "id": woo_attr_id, "name": woo_attr_name, "position": index, "visible": True, "options": [attrs[1]] }) return api_attributes def csv_attributes_to_api(self): """ Remove individual CSV attributes and create a single attributes for api """ for data in self._api_data: attr1 = data.pop("attribute_1_name") value1 = data.pop("attribute_1_value") attr2 = data.pop("attribute_2_name") value2 = data.pop("attribute_2_value") attr3 = data.pop("attribute_3_name") value3 = data.pop("attribute_3_value") attr4 = data.pop("attribute_4_name") value4 = data.pop("attribute_4_value") attributes = [(attr1, value1), (attr2, value2), (attr3, value3), (attr4, value4)] data["attributes"] = self.map_csv_attributes(attributes) def csv_dimensions_to_api(self): """ Remove individual dimensions and create a dimensions api attribute """ for data in self._api_data: length = data.pop("length") width = data.pop("width") height = data.pop("height") data["dimensions"] = self.map_csv_dimensions(length, width, height) def csv_images_to_api(self): """ Map csv images to api images """ for data in self._api_data: if "images" in data: data["images"] = self.map_csv_images(data["images"]) def map(self): """ Map data to api """ self.api_setup() for data in self._csv_data: mapped_data = dict() for key, value in data.items(): mapped_data[CSV_MAPPING[key]] = str(value) self._api_data.append(mapped_data) self.csv_category_to_api() self.csv_attributes_to_api() self.csv_dimensions_to_api() self.csv_images_to_api() @staticmethod def filter_category(category_list: List, category: str): """ Filter out the category based on category name. It will only return one category """ for cat in category_list: if cat["name"] == category: return cat return category_list.pop() @staticmethod def map_csv_dimensions(length: str, width: str, height: str): """ Create a dimensions attribute """ return {"length": length, "width": width, "height": height} @staticmethod def map_csv_images(images: str): """ Map csv images to api images """ api_images = [] images = [x.strip() for x in images.split(",")] for image in images: api_images.append({"src": image, "name": os.path.basename(image)}) return api_images @property def api_data(self): return self._api_data