Example #1
0
    def export(self):
        # Create ItemDefintion object
        if "wiki_name" in self.item_dict:
            del self.item_dict["wiki_name"]
        if "store_price" in self.item_dict:
            del self.item_dict["store_price"]
        if "seller" in self.item_dict:
            del self.item_dict["seller"]
        if "equipable_weapon" not in self.item_dict:
            self.item_dict["equipable_weapon"] = False

        for prop in self.properties:
            try:
                self.item_dict[prop]
            except KeyError:
                self.item_dict[prop] = None

        if self.item_dict["url"] == "https://oldschool.runescape.wiki/w/None":
            self.item_dict["url"] = None

        self.item_definition = ItemDefinition(**self.item_dict)
        self.compare_json_files(self.item_definition)
        json_out = self.item_definition.construct_json()
        # Actually output a JSON file, comment out for testing
        output_dir = os.path.join("..", "docs", "items-json")
        self.item_definition.export_json(True, output_dir)
        self.logger.debug(json_out)
        return
Example #2
0
    def compare_json_files(self, item_definition: ItemDefinition) -> bool:
        """Print the difference between this item object, and the item that exists in the database.

        :return changed: A boolean if the item is different, or not.
        """
        changed = False

        # Create JSON out object to compare
        current_json = item_definition.construct_json()

        # Try get existing entry (KeyError means it doesn't exist - aka a new item)
        try:
            existing_json = self.current_db[self.item_id]
        except KeyError:
            print(f">>> compare_json_files: NEW ITEM: {item_definition.id}")
            print(current_json)
            return changed

        if current_json == existing_json:
            return changed

        ddiff = DeepDiff(existing_json, current_json, ignore_order=True)
        print(f">>> compare_json_files: CHANGED ITEM: {item_definition.id}: {item_definition.name}")
        print(f"    {ddiff}")
        changed = True
        return changed
Example #3
0
 def export(self):
     # Create ItemDefintion object
     if "wiki_name" in self.item_dict:
         del self.item_dict["wiki_name"]
     if "store_price" in self.item_dict:
         del self.item_dict["store_price"]
     if "seller" in self.item_dict:
         del self.item_dict["seller"]
     self.itemDefinition = ItemDefinition(**self.item_dict)
     self.compare_json_files(self.itemDefinition)
     json_out = self.itemDefinition.construct_json()
     # Actually output a JSON file, comment out for testing
     output_dir = os.path.join("..", "docs", "items-json")
     self.itemDefinition.export_json(True, output_dir)
     self.logger.debug(json_out)
     return
Example #4
0
    def _load_item(self, item_json: Dict) -> None:
        """Convert the `item_json` into a :class:`ItemDefinition` and store it."""
        # Load the item using the ItemDefinition class
        item_def = ItemDefinition(**item_json)

        # Add item to list
        self.all_items.append(item_def)
        self.all_items_dict[item_def.id] = item_def
Example #5
0
    def _load_item(self, item_json: Dict) -> None:
        """Convert the `item_json` into a :class:`ItemDefinition` and store it.

        :param item_json: A dict from an open and loaded JSON file.
        :raises ValueError: Cannot populate item.
        """
        # Load the item using the ItemDefinition class
        try:
            item_def = ItemDefinition.from_json(item_json)
        except TypeError as e:
            raise ValueError("Error: Invalid JSON structure found, check supplied input. Exiting") from e

        # Add item to list
        self.all_items.append(item_def)
        self.all_items_dict[item_def.id] = item_def
Example #6
0
    def compare_json_files(self, itemDefinition: ItemDefinition) -> bool:
        """Print the difference between this item object, and the item that exists in the database.

        :return changed: A boolean if the item is different, or not.
        """
        changed = False
        changes = dict()

        # Create JSON out object to compare
        current_json = itemDefinition.construct_json()

        # Try get existing entry (KeyError means it doesn't exist - aka a new item)
        try:
            existing_json = self.current_db[self.item_id]
        except KeyError:
            return changed

        for prop in self.properties:
            if current_json[prop] != existing_json[prop]:
                changed = True
                changes[prop] = [current_json[prop], existing_json[prop]]

                # Also check equipable
                if itemDefinition.equipable_by_player:
                    for equipment_prop in self.equipment_properties:
                        try:
                            if current_json["equipment"][equipment_prop] != existing_json["equipment"][equipment_prop]:
                                changed = True
                                changes[equipment_prop] = [current_json["equipment"][equipment_prop],
                                                           existing_json["equipment"][equipment_prop]]
                        except KeyError:
                            pass  # This should be fixed, when old/new item has no equipment key

        # Print any item changes
        if changed:
            print(f">>>>>>>>>>> id: {itemDefinition.id}\tname: {itemDefinition.name}")
            for prop in changes:
                print("+++ MISMATCH!:", prop)
                print("TYPES:", type(changes[prop][1]), type(changes[prop][0]))
                print("OLD: %r" % changes[prop][1])
                print("NEW: %r" % changes[prop][0])
            print()

        return changed
Example #7
0
    def check_duplicate_item(self) -> ItemDefinition:
        """Determine if this is a duplicate item.

        :return: An ItemDeinition object.
        """
        # Start by setting the duplicate property to False
        self.item_dict["duplicate"] = False
        # Create an ItemDefinition object
        item_definition = ItemDefinition(**self.item_dict)

        # Set the item properties that we want to compare
        correlation_properties = {
            "wiki_name": False,
            "noted": False,
            "placeholder": False,
            "equipable": False,
            "equipable_by_player": False,
            "equipable_weapon": False
        }

        # Loop the list of currently (already processed) items
        for known_item in self.known_item_names:
            # Do a quick name check before deeper inspection
            if item_definition.name != known_item.name:
                continue

            # If the cache names are equal, do further inspection
            for cprop in correlation_properties:
                if getattr(item_definition,
                           cprop) == getattr(known_item, cprop):
                    correlation_properties[cprop] = True

            # Check is all values in correlation properties are True
            correlation_result = all(
                value is True for value in correlation_properties.values())
            if correlation_result:
                self.item_dict["duplicate"] = True

        return item_definition
Example #8
0
    def compare_json_files(self, item_definition: ItemDefinition) -> bool:
        """Print the difference between this item and the database."""
        # Create JSON out object to compare
        current_json = item_definition.construct_json()

        # Try get existing entry (KeyError means it doesn't exist - aka a new item)
        try:
            existing_json = self.all_db_items[self.item_id]
        except KeyError:
            print(f">>> compare_json_files: NEW ITEM: {item_definition.id}")
            print(current_json)
            return

        if current_json == existing_json:
            return

        ddiff = DeepDiff(existing_json, current_json, ignore_order=True)
        logging.debug(
            f">>> compare_json_files: CHANGED ITEM: {item_definition.id}: {item_definition.name}, {item_definition.wiki_name}"
        )
        print(
            f">>> compare_json_files: CHANGED ITEM: {item_definition.id}: {item_definition.name}"
        )

        # try:
        #     added_properties = ddiff["dictionary_item_added"]
        #     print("   ", added_properties)
        # except KeyError:
        #     pass

        # try:
        #     changed_properties = ddiff["values_changed"]
        #     for k, v in changed_properties.items():
        #         print("   ", k, v["new_value"])
        # except KeyError:
        #     pass

        print(ddiff)
        return
Example #9
0
class BuildItem:
    def __init__(self, item_id, item_json, wiki_text, normalized_names, buy_limits, skill_requirements, current_db,
                 weapon_types, weapon_stances):
        # Input item ID number
        self.item_id = item_id
        # Input JSON file (from RuneLite ItemScraper plugin)
        self.item_json = item_json

        # Input data
        self.wiki_text = wiki_text  # Dict of raw wiki text from OSRS Wiki
        self.normalized_names = normalized_names  # Maps cache names to OSRS Wiki names
        self.buy_limits = buy_limits  # Dictionary of item buy limits
        self.skill_requirements = skill_requirements  # Dictionary of item requirements
        self.current_db = current_db  # Dictionary dump of current database contents
        self.weapon_types = weapon_types  # Weapon type dictionary
        self.weapon_stances = weapon_stances  # Weapon stances dictionary

        # For this item, create dictionary for property storage
        self.item_dict = dict()

        # Setup logging
        logging.basicConfig(filename="builder.log",
                            filemode='a',
                            level=logging.DEBUG)
        self.logger = logging.getLogger(__name__)

        # If a page does not have a wiki page, it may be given a status number
        self.status_code = None

        self.properties = [
            "id",
            "name",
            "members",
            "tradeable",
            "tradeable_on_ge",
            "stackable",
            "noted",
            "noteable",
            "linked_id",
            "placeholder",
            "equipable",
            "equipable_by_player",
            "equipable_weapon",
            "cost",
            "lowalch",
            "highalch",
            "weight",
            "buy_limit",
            "quest_item",
            "release_date",
            "examine",
            "url"]

        self.equipment_properties = [
            "attack_stab",
            "attack_slash",
            "attack_crush",
            "attack_magic",
            "attack_ranged",
            "defence_stab",
            "defence_slash",
            "defence_crush",
            "defence_magic",
            "defence_ranged",
            "melee_strength",
            "ranged_strength",
            "magic_damage",
            "prayer",
            "slot",
            "requirements"]

        self.weapon_properties = [
            "attack_speed",
            "weapon_type",
            "stances"]

    def populate(self):
        """The primary entry and item object population function."""
        # Start section in logger
        self.logger.debug("============================================ START")
        self.logger.debug(f"item_id: {self.item_id}")

        # STAGE ONE: LOAD ITEM SCRAPER DATA
        self.logger.debug("STAGE ONE: Loading item cache data data to object...")

        self.populate_from_scraper()

        self.logger.debug(f'id: {self.item_dict["id"]}|name: {self.item_dict["name"]}')
        # print(f'>>> id: {self.item_dict["id"]}\tname: {self.item_dict["name"]}')

        # STAGE TWO: DETERMINE WIKI PAGE
        self.logger.debug("STAGE TWO: Determining OSRS Wiki page...")

        has_wiki_page = self.determine_wiki_page()

        # # This commented code can be used to determine wiki page normalization
        # # You must comment out the normalization lookup in determine_wiki_page
        # # WARNING: Does not check equipable item infobox extraction errors!
        # if not has_wiki_page:
        #     if str(self.item_dict["id"]) in self.normalized_names:
        #         normalized_name = self.normalized_names[str(self.item_dict["id"])][1]
        #         if normalized_name == "" or not normalized_name:
        #             normalized_name = self.item_dict["name"]
        #         status = self.normalized_names[str(self.item_dict["id"])][2]
        #         print(f"{self.item_dict["id"]}|{self.item_dict["name"]}|{normalized_name}|{status}")
        #     else:
        #         print(f"TODO:{self.item_dict["id"]}|{self.item_dict["name"]}|{self.item_dict["name"]}|X")

        if not has_wiki_page:
            # These will be items that cannot be processed and the program should exit
            print(f">>> not has_wiki_page: {self.item_dict['id']}, {self.item_dict['name']}")
            if self.item_dict["name"] == "" or self.item_dict["name"].lower() == "null":
                self.item_dict["equipable_by_player"] = False
                self.export()
                return

        # STAGE THREE: EXTRACT and PARSE INFOBOX
        self.logger.debug("STAGE THREE: Extracting the infobox...")

        # Extract the infobox for the item
        has_infobox = self.extract_infobox()

        # Handle the infobox extraction, depending on the item status code
        if has_infobox:
            self.logger.debug("INFOBOX: Success")
            self.parse_primary_infobox()
        elif self.status_code in [1, 2, 3, 4, 5]:
            self.logger.debug("INFOBOX: Invalid item saved")
            self.item_dict["url"] = None
            self.item_dict["equipable_by_player"] = False
            self.export()
            return
        elif self.status_code == 6:
            self.parse_primary_infobox()
            self.item_dict["equipable_by_player"] = False
            self.export()
            return
        else:
            self.logger.critical("INFOBOX: Extraction error.")
            if self.item_dict["name"] == "":
                self.export()
            quit()

        # STAGE FOUR: PARSE INFOBOX FOR EQUIPABLE ITEMS

        if self.item_dict["equipable"] and has_wiki_page:
            self.logger.debug("STAGE FIVE: Parsing the bonuses...")

            self.item_dict["equipment"] = dict()
            # Continue processing... but only if the item is equipable
            self.item_dict["equipable_by_player"] = True
            has_infobox_bonuses = self.extract_bonuses()
            if has_infobox_bonuses:
                self.logger.debug("Item InfoBox Bonuses extracted successfully")
            else:
                self.logger.critical("Item InfoBox Bonuses extraction error.")
                self.logger.critical("Status Code: %s" % self.status_code)
                self.item_dict["equipable_by_player"] = False
                self.export()
                # print(">>> ERROR: Could not determine equipable item bonuses...")
                return
        else:
            self.item_dict["equipable_by_player"] = False

        # STAGE FIVE: COMPARE TO CURRENT DATABASE CONTENTS
        self.logger.debug("STAGE FIVE: Compare object to existing database entry...")
        self.export()

    def export(self):
        # Create ItemDefintion object
        if "wiki_name" in self.item_dict:
            del self.item_dict["wiki_name"]
        if "store_price" in self.item_dict:
            del self.item_dict["store_price"]
        if "seller" in self.item_dict:
            del self.item_dict["seller"]
        if "equipable_weapon" not in self.item_dict:
            self.item_dict["equipable_weapon"] = False

        for prop in self.properties:
            try:
                self.item_dict[prop]
            except KeyError:
                self.item_dict[prop] = None

        if self.item_dict["url"] == "https://oldschool.runescape.wiki/w/None":
            self.item_dict["url"] = None

        self.item_definition = ItemDefinition(**self.item_dict)
        self.compare_json_files(self.item_definition)
        json_out = self.item_definition.construct_json()
        # Actually output a JSON file, comment out for testing
        output_dir = os.path.join("..", "docs", "items-json")
        self.item_definition.export_json(True, output_dir)
        self.logger.debug(json_out)
        return

    def populate_from_scraper(self):
        """Populate the itemDefinition object from the item-scraper file content."""
        self.item_dict["id"] = self.item_json["id"]
        self.item_dict["name"] = self.item_json["name"]
        self.item_dict["members"] = self.item_json["members"]
        self.item_dict["tradeable_on_ge"] = self.item_json["tradeable_on_ge"]
        self.item_dict["stackable"] = self.item_json["stackable"]
        self.item_dict["noted"] = self.item_json["noted"]
        self.item_dict["noteable"] = self.item_json["noteable"]
        self.item_dict["linked_id"] = self.item_json["linked_id"]
        self.item_dict["placeholder"] = self.item_json["placeholder"]
        self.item_dict["equipable"] = self.item_json["equipable"]
        self.item_dict["cost"] = self.item_json["cost"]
        self.item_dict["lowalch"] = self.item_json["lowalch"]
        self.item_dict["highalch"] = self.item_json["highalch"]

    def determine_wiki_page(self):
        """Determine the OSRS Wiki page/url/name using the item name."""
        # Set the initial wiki_name property to the actual item name
        # This may change depending on the item lookup success/failure
        wiki_name = self.item_dict["name"]

        # PHASE ONE: Check if the item name is in the OSRS Wiki item dump using normalized name

        if str(self.item_dict["id"]) in self.normalized_names:
            self.logger.debug(">>> ITEM FOUND IN NORMALIZED")
            # Determine normalized wiki name using lookup
            normalized_name = self.normalized_names[str(self.item_dict["id"])][1]
            # Set wiki URL and name
            wiki_url = normalized_name.replace(" ", "_")
            wiki_url = wiki_url.replace("'", "%27")
            wiki_url = wiki_url.replace("&", "%26")
            wiki_url = wiki_url.replace("+", "%2B")
            self.item_dict["url"] = f"https://oldschool.runescape.wiki/w/{wiki_url}"
            self.item_dict["wiki_name"] = normalized_name
            # Set item status code
            self.status_code = int(self.normalized_names[str(self.item_dict["id"])][2])
            # Item name found in dump using normalization, return True
            return True

        # PHASE TWO: Check if the item name is in the OSRS Wiki item dump (no normalization)

        if wiki_name in self.wiki_text:
            self.logger.debug(">>> ITEM FOUND")
            # Set wiki URL and name
            wiki_url = self.item_dict["name"].replace(" ", "_")
            wiki_url = wiki_url.replace("'", "%27")
            wiki_url = wiki_url.replace("&", "%26")
            wiki_url = wiki_url.replace("+", "%2B")
            self.item_dict["url"] = f"https://oldschool.runescape.wiki/w/{wiki_url}"
            self.item_dict["wiki_name"] = self.item_dict["name"]
            # Set item status code, try/except as it may not be in list
            try:
                self.status_code = int(self.normalized_names[str(self.item_dict["name"])][2])
            except KeyError:
                self.status_code = 0  # Zero is no issue with item, direct lookup
            # Item name found in dump without normalization, return True
            return True

        # If we got this far, the wiki page was not found, return false
        self.logger.debug(">>> ITEM NOT FOUND")
        return False

    def extract_infobox(self):
        """Extract the primary properties and bonuses for the item."""
        # Set templates
        self.template_primary = None
        self.template_bonuses = None

        try:
            wiki_text_entry = self.wiki_text[self.item_dict["wiki_name"]]
            wikicode = mwparserfromhell.parse(wiki_text_entry)
        except KeyError:
            # The wiki_name was not found in the available dumped wikitext pages
            # Return false to indicate no wikitext was extracted
            self.logger.debug("extract_infobox: KeyError for self.wikitext")
            return False

        # Loop through templates in wikicode from wiki page
        # Then call Inforbox Item processing method
        templates = wikicode.filter_templates()
        for template in templates:
            template_name = template.name.strip()
            template_name = template_name.lower()
            if "infobox item" in template_name:
                self.template_primary = template
            if "infobox bonuses" in template_name:
                self.template_bonuses = template
            if "infobox construction" in template_name:
                self.template_primary = template
            if "infobox pet" in template_name:
                self.template_primary = template

        # If no template_primary was found, return false
        if not self.template_primary:
            self.logger.debug("extract_infobox: not self.template_primary")
            return False

        # If any equipable item, and no bonuses was found, return false
        if self.item_dict["equipable"] and not self.template_bonuses:
            self.logger.debug("extract_infobox: not self.template_bonuses")
            return False

        # If we got this far, return true
        return True

    def parse_primary_infobox(self):
        """Parse an actual Infobox template."""
        template = self.template_primary
        # Set defaults for versioned infoboxes
        is_versioned = False  # has multiple versions available
        self.current_version = None  # The version that matches the item
        version_count = 0  # the number of versions available

        # STAGE ONE: Determine if we have a versioned infobox, and the version count

        version_identifiers = ["version",
                               "name",
                               "itemname"]

        for version_identifier in version_identifiers:
            # Check if the infobox is versioned, and get a version count
            if version_count == 0:
                try:
                    template.get(version_identifier + "1").value
                    is_versioned = True
                    # Now, try to determine how many versions are present
                    i = 1
                    while i <= 20:  # Guessing max version number is 20
                        try:
                            template.get(version_identifier + "1").value
                            version_count += 1
                        except ValueError:
                            break
                        i += 1
                except ValueError:
                    pass

        # STAGE TWO: Match a versioned infobox to the item name

        if is_versioned:
            # Try determine
            for version_identifier in version_identifiers:
                try:
                    template.get(version_identifier + "1").value
                    i = 1
                    while i <= version_count:
                        versioned_name = version_identifier + str(i)
                        if self.item_dict["name"] == template.get(versioned_name).value.strip():
                            self.current_version = i
                            break
                        i += 1
                except ValueError:
                    pass

            self.logger.debug("NOTE: versioned infobox: %s" % self.current_version)

        if is_versioned and self.current_version is None:
            self.current_version = 1

        # WEIGHT: Determine the weight of an item ()
        weight = None
        if self.current_version is not None:
            key = "weight" + str(self.current_version)
            weight = self.extract_infobox_value(template, key)
        if weight is None:
            weight = self.extract_infobox_value(template, "weight")
        if weight is not None:
            self.item_dict["weight"] = infobox_cleaner.clean_weight(weight, self.item_id)

        # QUEST: Determine if item is associated with a quest ()
        quest = None
        if self.current_version is not None:
            key = "quest" + str(self.current_version)
            quest = self.extract_infobox_value(template, key)
        if quest is None:
            quest = self.extract_infobox_value(template, "quest")
        if quest is not None:
            self.item_dict["quest_item"] = infobox_cleaner.clean_quest(quest)

        # Determine the release date of an item ()
        release_date = None
        if self.current_version is not None:
            key = "release" + str(self.current_version)
            release_date = self.extract_infobox_value(template, key)
        if release_date is None:
            release_date = self.extract_infobox_value(template, "release")
        if release_date is not None:
            self.item_dict["release_date"] = infobox_cleaner.clean_release_date(release_date)

        # Determine if item has a store price ()
        store_price = None
        if self.current_version is not None:
            key = "store" + str(self.current_version)
            store_price = self.extract_infobox_value(template, key)
        if store_price is None:
            store_price = self.extract_infobox_value(template, "store")
        if store_price is not None:
            self.item_dict["store_price"] = infobox_cleaner.clean_store_price(store_price)

        # Determine if item has a store price ()
        seller = None
        if self.current_version is not None:
            key = "seller" + str(self.current_version)
            seller = self.extract_infobox_value(template, key)
        if seller is None:
            seller = self.extract_infobox_value(template, "seller")
        if seller is not None:
            self.item_dict["seller"] = infobox_cleaner.clean_seller(seller)

        # Determine the examine text of an item ()
        tradeable = None
        if self.current_version is not None:
            key = "tradeable" + str(self.current_version)
            tradeable = self.extract_infobox_value(template, key)
        if tradeable is None:
            tradeable = self.extract_infobox_value(template, "tradeable")
        if tradeable is not None:
            self.item_dict["tradeable"] = infobox_cleaner.clean_tradeable(tradeable)
        else:
            self.item_dict["tradeable"] = False

        # Determine the examine text of an item ()
        examine = None
        if self.current_version is not None:
            key = "examine" + str(self.current_version)
            examine = self.extract_infobox_value(template, key)
        if examine is None:
            examine = self.extract_infobox_value(template, "examine")
        if examine is not None:
            self.item_dict["examine"] = infobox_cleaner.clean_examine(examine, self.item_dict["name"])
        else:
            # Being here means the extraction for "examine" failed
            key = "itemexamine" + str(self.current_version)
            examine = self.extract_infobox_value(template, key)
            if examine is None:
                examine = self.extract_infobox_value(template, "itemexamine")
            if examine is not None:
                self.item_dict["examine"] = infobox_cleaner.clean_examine(examine, self.item_dict["name"])

        # Determine if item has a buy limit ()
        if not self.item_dict["tradeable"]:
            self.item_dict["buy_limit"] = None
        else:
            try:
                self.item_dict["buy_limit"] = int(self.buy_limits[self.item_dict["name"]])
                if self.item_dict["noted"]:
                    self.item_dict["buy_limit"] = None
            except KeyError:
                self.item_dict["buy_limit"] = None

        return True

    def extract_infobox_value(self, template: mwparserfromhell.nodes.template.Template, key: str) -> str:
        """Helper method to extract a value from a template using a specified key.

        This helper method is a simple solution to repeatedly try to fetch a specific
        entry from a wiki text template (a mwparserfromhell template object).

        :param template: A mediawiki wiki text template.
        :param key: The key to query in the template.
        :return value: The extracted template value based on supplied key.
        """
        value = None
        try:
            value = template.get(key).value
            value = value.strip()
            return value
        except ValueError:
            return value

    def extract_bonuses(self) -> bool:
        """Extract the infobox bonuses template from raw wikitext.

        :return: If the infobox bonuses template was extracted successfully or not.
        """
        # Extract Infobox Bonuses from wikitext
        try:
            wikicode = mwparserfromhell.parse(self.wiki_text[self.item_dict["wiki_name"]])
        except KeyError:
            return False
        templates = wikicode.filter_templates()
        for template in templates:
            if "infobox bonuses" in template.lower():
                extracted_infobox = self.parse_bonuses(template)
                if extracted_infobox:
                    return True

        return False

    def parse_bonuses(self, template: mwparserfromhell.nodes.template.Template) -> bool:
        """Parse the wiki text template and extract item bonus values from it.

        :param template: A mediawiki wiki text template.
        """
        self.item_dict["equipment"]["attack_stab"] = self.clean_bonuses_value(template, "astab")
        self.item_dict["equipment"]["attack_slash"] = self.clean_bonuses_value(template, "aslash")
        self.item_dict["equipment"]["attack_crush"] = self.clean_bonuses_value(template, "acrush")
        self.item_dict["equipment"]["attack_magic"] = self.clean_bonuses_value(template, "amagic")
        self.item_dict["equipment"]["attack_ranged"] = self.clean_bonuses_value(template, "arange")
        self.item_dict["equipment"]["defence_stab"] = self.clean_bonuses_value(template, "dstab")
        self.item_dict["equipment"]["defence_slash"] = self.clean_bonuses_value(template, "dslash")
        self.item_dict["equipment"]["defence_crush"] = self.clean_bonuses_value(template, "dcrush")
        self.item_dict["equipment"]["defence_magic"] = self.clean_bonuses_value(template, "dmagic")
        self.item_dict["equipment"]["defence_ranged"] = self.clean_bonuses_value(template, "drange")
        self.item_dict["equipment"]["melee_strength"] = self.clean_bonuses_value(template, "str")
        self.item_dict["equipment"]["ranged_strength"] = self.clean_bonuses_value(template, "rstr")
        self.item_dict["equipment"]["magic_damage"] = self.clean_bonuses_value(template, "mdmg")
        self.item_dict["equipment"]["prayer"] = self.clean_bonuses_value(template, "prayer")

        # Determine the slot for the equipable item
        self.item_dict["equipment"]["slot"] = None
        try:
            self.item_dict["equipment"]["slot"] = self.strip_infobox(template.get("slot").value)
            self.item_dict["equipment"]["slot"] = self.item_dict["equipment"]["slot"].lower()
        except ValueError:
            self.item_dict["equipment"]["slot"] = None
            self.logger.critical("Could not determine equipable item slot")
            quit()

        # Determine the skill requirements for the equipable item
        self.item_dict["equipment"]["requirements"] = None
        try:
            requirements = self.skill_requirements[str(self.item_id)]
            self.item_dict["equipment"]["requirements"] = requirements
        except KeyError:
            self.item_dict["equipment"]["requirements"] = None

        # Start processing only weapons

        # If item is weapon, two-handed, or 2h, start processing the weapon data
        if (self.item_dict["equipment"]["slot"] == "weapon" or
                self.item_dict["equipment"]["slot"] == "two-handed" or
                self.item_dict["equipment"]["slot"] == "2h"):

            self.item_dict["weapon"] = dict()

            # Try set the attack speed of the weapon
            try:
                self.item_dict["weapon"]["attack_speed"] = int(self.strip_infobox(template.get("aspeed").value))
            except ValueError:
                self.item_dict["weapon"]["attack_speed"] = None
                self.logger.critical("WEAPON: Could not determine weapon attack speed")

                # Item IDs with no known attack speed, set to zero
                if int(self.item_id) in [8871]:
                    self.item_dict["weapon"]["attack_speed"] = 0
                # Salamander fix, set to base attack speed of 5
                elif int(self.item_id) in [10145, 10146, 10147, 10147, 10148, 10149]:
                    self.item_dict["weapon"]["attack_speed"] = 5
                else:
                    quit()

            # Try to set the weapon type of the weapon
            try:
                weapon_type = self.weapon_types[str(self.item_dict["id"])]["weapon_type"]
                self.item_dict["weapon"]["weapon_type"] = weapon_type
            except KeyError:
                self.item_dict["weapon"]["weapon_type"] = None
                self.logger.critical("WEAPON: Could not determine weapon type")
                quit()

            # Try to set stances available for the weapon
            try:
                self.item_dict["weapon"]["stances"] = self.weapon_stances[self.item_dict["weapon"]["weapon_type"]]
            except KeyError:
                self.item_dict["weapon"]["stances"] = None
                self.logger.critical("WEAPON: Could not determine weapon stances")
                quit()

            # Finally, set the equipable_weapon property to true
            self.item_dict["equipable_weapon"] = True

        return True

    def clean_bonuses_value(self, template: mwparserfromhell.nodes.template.Template, prop: str):
        """Clean a item bonuses value extracted from a wiki template.

        :param template: A mediawiki wiki text template.
        :param prop: The key to query in the template.
        :return value: The extracted template value that has been int cast.
        """
        value = None

        # Try and get the versioned infobox value
        if self.current_version is not None:
            key = prop + str(self.current_version)
            value = self.extract_infobox_value(template, key)

        # If unsuccessful, try and get the normal infoxbox value
        if value is None:
            value = self.extract_infobox_value(template, prop)

        if value is not None:
            value = self.strip_infobox(value)
            if isinstance(value, str):
                if value[0] == "-":
                    if value[1:].isdigit():
                        value = int(value)
                elif value[0] == "+":
                    if value[1:].isdigit():
                        value = int(value)
                else:
                    if value.isdigit():
                        value = int(value)
        else:
            value = 0

        return value

    def strip_infobox(self, value: str) -> str:
        """Generic infobox wiki text cleaner.

        :return clean_value: A cleaned wiki text value.
        """
        # Clean an passed InfoBox string
        clean_value = str(value)
        clean_value = clean_value.strip()
        clean_value = clean_value.replace("[", "")
        clean_value = clean_value.replace("]", "")
        return clean_value

    def compare_json_files(self, item_definition: ItemDefinition) -> bool:
        """Print the difference between this item object, and the item that exists in the database.

        :return changed: A boolean if the item is different, or not.
        """
        changed = False

        # Create JSON out object to compare
        current_json = item_definition.construct_json()

        # Try get existing entry (KeyError means it doesn't exist - aka a new item)
        try:
            existing_json = self.current_db[self.item_id]
        except KeyError:
            print(f">>> compare_json_files: NEW ITEM: {item_definition.id}")
            print(current_json)
            return changed

        if current_json == existing_json:
            return changed

        ddiff = DeepDiff(existing_json, current_json, ignore_order=True)
        print(f">>> compare_json_files: CHANGED ITEM: {item_definition.id}: {item_definition.name}")
        print(f"    {ddiff}")
        changed = True
        return changed
Example #10
0
class BuildItem:
    def __init__(self, item_id, all_item_cache_data, all_wikitext_processed,
                 all_wikitext_raw, all_db_items, buy_limits_data,
                 skill_requirements_data, weapon_types_data,
                 weapon_stances_data, invalid_items_data, known_item_names,
                 export_item):
        self.item_id = item_id
        self.all_item_cache_data = all_item_cache_data  # Raw cache data for all items
        self.all_wikitext_processed = all_wikitext_processed  # Processed wikitext for all items
        self.all_wikitext_raw = all_wikitext_raw  # Raw data dump from OSRS Wiki
        self.all_db_items = all_db_items  # All current item database contents
        self.buy_limits_data = buy_limits_data  # Dictionary of item buy limits
        self.skill_requirements = skill_requirements_data  # Dictionary of item requirements
        self.weapon_types_data = weapon_types_data  # Weapon type dictionary
        self.weapon_stances_data = weapon_stances_data  # Weapon stances dictionary
        self.invalid_items_data = invalid_items_data  # Dictionary of invalid items
        self.known_item_names = known_item_names  # A list of already known (processed) items
        self.export_item = export_item  # If the JSON should be exported/created

        # For this item instance, create dictionary for property storage
        self.item_dict = dict()

        # Set some important properties used for item building
        self.wiki_page_name = None  # The page name the wikitext is from
        self.infobox_version_number = None  # The version used on the wikitext page
        self.status = None  # Used if the item is special (invalid, normalized etc.)
        self.is_invalid_item = False  # If the item is not found using ID, linked ID or name lookup

        # All properties that are available for all items
        self.properties = [
            "id", "name", "incomplete", "members", "tradeable",
            "tradeable_on_ge", "stackable", "noted", "noteable",
            "linked_id_item", "linked_id_noted", "linked_id_placeholder",
            "placeholder", "equipable", "equipable_by_player",
            "equipable_weapon", "cost", "lowalch", "highalch", "weight",
            "buy_limit", "quest_item", "release_date", "examine", "wiki_name",
            "wiki_url"
        ]

        # Additional properties for all equipable items (weapons/armour)
        self.equipment_properties = [
            "attack_stab", "attack_slash", "attack_crush", "attack_magic",
            "attack_ranged", "defence_stab", "defence_slash", "defence_crush",
            "defence_magic", "defence_ranged", "melee_strength",
            "ranged_strength", "magic_damage", "prayer", "slot", "requirements"
        ]

        # Additional properties for all equipable weapons
        self.weapon_properties = ["attack_speed", "weapon_type", "stances"]

    def check_duplicate_item(self) -> ItemDefinition:
        """Determine if this is a duplicate item.

        :return: An ItemDeinition object.
        """
        # Start by setting the duplicate property to False
        self.item_dict["duplicate"] = False
        # Create an ItemDefinition object
        item_definition = ItemDefinition(**self.item_dict)

        # Set the item properties that we want to compare
        correlation_properties = {
            "wiki_name": False,
            "noted": False,
            "placeholder": False,
            "equipable": False,
            "equipable_by_player": False,
            "equipable_weapon": False
        }

        # Loop the list of currently (already processed) items
        for known_item in self.known_item_names:
            # Do a quick name check before deeper inspection
            if item_definition.name != known_item.name:
                continue

            # If the cache names are equal, do further inspection
            for cprop in correlation_properties:
                if getattr(item_definition,
                           cprop) == getattr(known_item, cprop):
                    correlation_properties[cprop] = True

            # Check is all values in correlation properties are True
            correlation_result = all(
                value is True for value in correlation_properties.values())
            if correlation_result:
                self.item_dict["duplicate"] = True

        return item_definition

    def generate_item_object(self):
        """Generate the `ItemDefinition` object from the item_dict dictionary."""
        self.item_definition = ItemDefinition(**self.item_dict)

    def compare_new_vs_old_item(self):
        """Compare the newly generated item to the existing item in the database."""
        self.compare_json_files(self.item_definition)

    def export_item_to_json(self):
        """Export item to JSON, if requested."""
        if self.export_item:
            output_dir = os.path.join("..", "docs", "items-json")
            self.item_definition.export_json(True, output_dir)
        logging.debug(self.item_dict)

    def preprocessing(self) -> Dict:
        """Preprocess an item, and set important object variables.

        This function preprocesses every item dumped from the OSRS cache. Various
        properties are set to help further processing. Items are determined if
        they are a linked item (noted/placeholder), or an actual item. The item
        is checked if it is a valid item (has a wiki page, is an actual item etc.).
        Finally, the wikitext (from the OSRS wiki) is found by looking up ID, linked
        ID, name, and normalized name. The `Infobox Item` or `Infobox Pet` is then
        extracted so that the wiki properties can be later processed and populated.

        :return: A dictionary including success and code.
        """
        # Initialize dictionary to return preprocessing status
        return_status = {"status": False, "code": None}

        # Set item ID variables
        self.item_id_int = int(self.item_id)  # Item ID number as an integer
        self.item_id_str = str(self.item_id)  # Item ID number as a string

        # Load item dictionary of cache data based on item ID
        # This raw cache data is the baseline information about the specific item
        # and can be considered 100% correct and available for every item
        self.item_cache_data = self.all_item_cache_data[self.item_id_str]

        # Set item name variable (directly from the cache dump)
        self.item_name = self.item_cache_data["name"]

        # Log and print item
        logging.debug(
            f"======================= {self.item_id_str} {self.item_name}")
        # print(f"======================= {self.item_id_str} {self.item_name}")
        logging.debug(f"preprocessing: using the following cache data:")
        logging.debug(self.item_cache_data)

        # Get the linked ID item value, if available
        self.linked_id_item_int = None
        self.linked_id_item_str = None
        if self.item_cache_data["linked_id_item"] is not None:
            self.linked_id_item_int = int(
                self.item_cache_data["linked_id_item"])
            self.linked_id_item_str = str(
                self.item_cache_data["linked_id_item"])
        logging.debug(
            f"preprocessing: Linked item ID: {self.linked_id_item_str}")

        # Determine the ID number to extract
        # Noted and placeholder items should use the linked_id_item property
        # to fill in additional wiki data...
        self.item_id_to_process_int = None
        self.item_id_to_process_str = None
        if self.item_cache_data["noted"] is True or self.item_cache_data[
                "placeholder"] is True:
            self.item_id_to_process_int = int(self.linked_id_item_int)
            self.item_id_to_process_str = str(self.linked_id_item_str)
        else:
            self.item_id_to_process_int = int(self.item_id)
            self.item_id_to_process_str = str(self.item_id)
        logging.debug(
            f"preprocessing: ID to process: {self.item_id_to_process_str}")

        # Find the wiki page
        # Set all variables to None (for invalid items)
        self.item_wikitext = None
        self.wikitext_found_using = None
        self.has_infobox = False

        # Try to find the wiki data using direct ID number search
        if self.all_wikitext_processed.get(self.item_id_str, None):
            self.item_wikitext = self.all_wikitext_processed.get(
                self.item_id_str, None)
            self.wikitext_found_using = "id"
            return_status["code"] = "lookup_passed_id"

        # Try to find the wiki data using linked_id_item ID number search
        elif self.all_wikitext_processed.get(self.linked_id_item_str, None):
            self.item_wikitext = self.all_wikitext_processed.get(
                self.linked_id_item_str, None)
            self.wikitext_found_using = "linked_id"
            return_status["code"] = "lookup_passed_linked_id"

        # Try to find the wiki data using direct name search
        elif self.all_wikitext_raw.get(self.item_name, None):
            self.item_wikitext = self.all_wikitext_raw.get(
                self.item_name, None)
            self.wikitext_found_using = "name"
            return_status["code"] = "lookup_passed_name"

        if self.item_id_to_process_str in self.invalid_items_data:
            # Anything here means item cannot be found by id, linked_id, or name
            # This can include not being an actual item, has no wiki page etc.
            # The item must be invalid, handle it accordingly
            self.is_invalid_item = True
            try:
                self.status = self.invalid_items_data[
                    self.item_id_to_process_str]["status"]
                self.normalized_name = self.invalid_items_data[
                    self.item_id_to_process_str]["normalized_name"]
            except KeyError:
                self.status = None
                self.normalized_name = None
            logging.debug(
                f"preprocessing: Invalid item details: {self.is_invalid_item} {self.status} {self.normalized_name}"
            )

            # Try to find the wiki data using normalized_name search
            if self.all_wikitext_raw.get(self.normalized_name, None):
                self.item_wikitext = self.all_wikitext_raw.get(
                    self.normalized_name, None)
                self.wikitext_found_using = "normalized_name"
                return_status["code"] = "valid"
            else:
                return_status["code"] = "lookup_failed"

        logging.debug(
            f"preprocessing: self.item_wikitext found using: {self.wikitext_found_using}"
        )

        # If there is no wikitext, and the item is valid, raise a critical error
        if not self.item_wikitext and not self.is_invalid_item:
            logging.critical(
                "CRITICAL: Could not find item_wikitext by id, linked_id_item or name..."
            )
            return_status["code"] = "no_item_wikitext"
            return return_status

        # Parse the infobox item
        infobox_parser = WikitextTemplateParser(self.item_wikitext)

        # Try extract infobox for item, then pet
        self.has_infobox = infobox_parser.extract_infobox("infobox item")
        if not self.has_infobox:
            self.has_infobox = infobox_parser.extract_infobox("infobox pet")
            if not self.has_infobox:
                self.template = None
                logging.critical("CRITICAL: Could not find template...")
                return_status["code"] = "no_infobox_template"
                return return_status

        self.is_versioned = infobox_parser.determine_infobox_versions()
        logging.debug(
            f"preprocessing: Is the infobox versioned: {self.is_versioned}")
        self.versioned_ids = infobox_parser.extract_infobox_ids()
        logging.debug(f"preprocessing: Versioned IDs: {self.versioned_ids}")

        # Set the infobox version number, default to empty string (no version number)
        try:
            if self.versioned_ids:
                self.infobox_version_number = self.versioned_ids[
                    self.item_id_to_process_int]
        except KeyError:
            if self.is_versioned:
                self.infobox_version_number = "1"
            else:
                self.infobox_version_number = ""
        logging.debug(
            f"preprocessing: infobox_version_number: {self.infobox_version_number}"
        )

        # Set the template
        self.template = infobox_parser.template

        return_status["status"] = True
        return return_status

    def populate_item(self):
        """Populate an item after preprocessing it.

        This is called for every item in the OSRS cache dump. Start by populating the
        raw metadata from the cache. Then process invalid items, and """
        # Start by populating the item from the cache data
        self.populate_from_cache_data()

        # Process an invalid item
        if self.is_invalid_item:
            logging.debug(
                "populate_item: Found and processing an invalid item...")

            if self.status == "unequipable":
                # Cache thinks the item is equipable, but it is not
                self.populate_item_properties_from_wiki_data()
                self.item_dict["equipable_by_player"] = False
                self.item_dict["equipable_weapon"] = False
                self.item_dict["incomplete"] = True
                return True

            if self.status == "no_bonuses_available":
                # Equipable item with wiki page, but does not have an Infobox Bonuses template
                # This is only ever called on ring slot items, as they sometimes have a
                # wiki page without an Infobox Bonuses template
                self.populate_non_wiki_item()
                self.item_dict["equipment"] = dict()
                for equipment_property in self.equipment_properties:
                    self.item_dict["equipment"][equipment_property] = 0
                self.item_dict["equipment"]["slot"] = "ring"
                self.item_dict["equipment"]["requirements"] = None
                self.item_dict["equipable_by_player"] = True
                self.item_dict["equipable_weapon"] = False
                self.item_dict["incomplete"] = True
                return True

            if self.status == "normalized":
                # Some items have a wiki page, but lookup by ID, linked ID and item name
                # fail. So use the normalized name from the invalid-items.json file
                self.item_wikitext = self.all_wikitext_raw[
                    self.normalized_name]
                self.wikitext_found_using = "normalized_name"
                infobox_parser = WikitextTemplateParser(self.item_wikitext)
                infobox_parser.extract_infobox("infobox item")
                self.template = infobox_parser.template
                self.populate_item_properties_from_wiki_data()
                self.item_dict["equipable_by_player"] = False
                self.item_dict["equipable_weapon"] = False
                return True

            if self.status == "unobtainable":
                # Some items are unobtainable, set defaults
                self.populate_non_wiki_item()
                return True

            if self.status == "skill_guide_icon":
                # Some items are actually an icon in a skill guide, set defaults
                self.populate_non_wiki_item()
                return True

            if self.status == "construction_icon":
                # Some items are actually an icon in the construction interface, set defaults
                self.populate_non_wiki_item()
                return True

            if self.status == "unhandled":
                # Some items have not been classified, set defaults
                self.populate_non_wiki_item()
                return True

        if not self.item_dict["equipable"]:
            # Process a normal, non-equipable item
            logging.debug(
                "populate_item: Populating a normal item using wiki data...")
            self.populate_item_properties_from_wiki_data()
            self.item_dict["equipable_by_player"] = False
            self.item_dict["equipable_weapon"] = False
            return True

        if self.item_dict["equipable"]:
            # Process an equipable item
            logging.debug(
                "populate_item: Populating an equipable item using wiki data..."
            )
            self.populate_item_properties_from_wiki_data()
            self.populate_equipable_properties_from_wiki_data()
            return True

        # Return false by default, this means the item was not found or processed
        logging.error("populate_item: Item was not processed...")
        return False

    def populate_non_wiki_item(self):
        """Populate an iem that has no wiki page."""
        # Set all item properties to None if they have not been populated
        for prop in self.properties:
            try:
                self.item_dict[prop]
            except KeyError:
                self.item_dict[prop] = None

        self.item_dict["tradeable"] = False
        self.item_dict["quest_item"] = False
        # Set equipable item/weapon properties to false
        self.item_dict["equipable_by_player"] = False
        self.item_dict["equipable_weapon"] = False
        self.item_dict["incomplete"] = True

    def populate_from_cache_data(self):
        """Populate an item using raw cache data.

        This function takes the raw OSRS cache data for the specific item and loads
        all available properties (that are extracted from the cache).
        """
        # Log, then populate cache properties
        logging.debug(
            "populate_from_cache: Loading item cache data data to object...")
        self.item_dict["id"] = self.item_cache_data["id"]
        self.item_dict["name"] = self.item_cache_data["name"]
        self.item_dict["members"] = self.item_cache_data["members"]
        self.item_dict["tradeable_on_ge"] = self.item_cache_data[
            "tradeable_on_ge"]
        self.item_dict["stackable"] = self.item_cache_data["stackable"]
        self.item_dict["noted"] = self.item_cache_data["noted"]
        self.item_dict["noteable"] = self.item_cache_data["noteable"]
        self.item_dict["linked_id_item"] = self.item_cache_data[
            "linked_id_item"]
        self.item_dict["linked_id_noted"] = self.item_cache_data[
            "linked_id_noted"]
        self.item_dict["linked_id_placeholder"] = self.item_cache_data[
            "linked_id_placeholder"]
        self.item_dict["placeholder"] = self.item_cache_data["placeholder"]
        self.item_dict["equipable"] = self.item_cache_data["equipable"]
        self.item_dict["cost"] = self.item_cache_data["cost"]
        self.item_dict["lowalch"] = self.item_cache_data["lowalch"]
        self.item_dict["highalch"] = self.item_cache_data["highalch"]

    def populate_item_properties_from_wiki_data(self):
        """Populate item data from a OSRS Wiki Infobox Item template."""
        if not self.has_infobox:
            # Cannot populate if there is no infobox!
            self.populate_non_wiki_item()
            logging.error(
                "populate_item_properties_from_wiki_data: No infobox for wiki item."
            )
            return False

        # STAGE ONE: Determine then set the wiki_name and wiki_url

        # Manually set OSRS Wiki name
        if self.wikitext_found_using not in ["id", "linked_id"]:
            # Item found in wiki by ID, cache name is the best option
            wiki_page_name = self.item_name
        elif self.wikitext_found_using == "normalized":
            # Item found in wiki by normalized name, normalize name is used
            wiki_page_name = self.normalized_name
        else:
            # Item found using direct cache name lookup on wiki page names,
            # So use wiki page name in the item_wikitext array
            wiki_page_name = self.item_wikitext[0]

        wiki_versioned_name = None
        wiki_name = None

        # Get the versioned, or non-versioned, name from the infobox
        if self.infobox_version_number is not None:
            key = "version" + str(self.infobox_version_number)
            wiki_versioned_name = self.extract_infobox_value(
                self.template, key)
        else:
            wiki_versioned_name = self.extract_infobox_value(
                self.template, "version")

        # Set the wiki_name property
        if wiki_versioned_name is not None:
            if wiki_versioned_name.startswith("("):
                wiki_name = wiki_page_name + " " + wiki_versioned_name
            else:
                wiki_name = wiki_page_name + " (" + wiki_versioned_name + ")"
        else:
            wiki_name = wiki_page_name
            self.item_dict["incomplete"] = True

        self.item_dict["wiki_name"] = wiki_name

        # Set the wiki_url property
        if wiki_versioned_name is not None:
            wiki_url = wiki_page_name + "#" + wiki_versioned_name
        else:
            wiki_url = wiki_page_name
            self.item_dict["incomplete"] = True

        wiki_url = wiki_url.replace(" ", "_")
        self.item_dict[
            "wiki_url"] = "https://oldschool.runescape.wiki/w/" + wiki_url

        # STAGE TWO: Extract, process and set item properties from the infobox template

        # WEIGHT: Determine the weight of an item
        weight = None
        if self.infobox_version_number is not None:
            key = "weight" + str(self.infobox_version_number)
            weight = self.extract_infobox_value(self.template, key)
        if weight is None:
            weight = self.extract_infobox_value(self.template, "weight")
        if weight is not None:
            self.item_dict["weight"] = infobox_cleaner.clean_weight(
                weight, self.item_id)
        else:
            self.item_dict["weight"] = None
            self.item_dict["incomplete"] = True

        # QUEST: Determine if item is associated with a quest
        quest = None
        if self.infobox_version_number is not None:
            key = "quest" + str(self.infobox_version_number)
            quest = self.extract_infobox_value(self.template, key)
        if quest is None:
            quest = self.extract_infobox_value(self.template, "quest")
        if quest is not None:
            self.item_dict["quest_item"] = infobox_cleaner.clean_quest(quest)
        else:
            # Being here means the extraction for "quest" failed
            key = "questrequired" + str(self.infobox_version_number)
            quest = self.extract_infobox_value(self.template, key)
            if quest is None:
                quest = self.extract_infobox_value(self.template,
                                                   "questrequired")
            if quest is not None:
                self.item_dict["quest_item"] = infobox_cleaner.clean_quest(
                    quest)
            else:
                self.item_dict["quest_item"] = False

        # Determine the release date of an item
        release_date = None
        if self.infobox_version_number is not None:
            key = "release" + str(self.infobox_version_number)
            release_date = self.extract_infobox_value(self.template, key)
        if release_date is None:
            release_date = self.extract_infobox_value(self.template, "release")
        if release_date is not None:
            self.item_dict[
                "release_date"] = infobox_cleaner.clean_release_date(
                    release_date)
        else:
            self.item_dict["release_date"] = None
            self.item_dict["incomplete"] = True

        # Determine if an item is tradeable
        tradeable = None
        if self.infobox_version_number is not None:
            key = "tradeable" + str(self.infobox_version_number)
            tradeable = self.extract_infobox_value(self.template, key)
        if tradeable is None:
            tradeable = self.extract_infobox_value(self.template, "tradeable")
        if tradeable is not None:
            self.item_dict["tradeable"] = infobox_cleaner.clean_boolean(
                tradeable)
        else:
            self.item_dict["tradeable"] = False
            self.item_dict["incomplete"] = True

        # Determine the examine text of an item
        examine = None
        if self.infobox_version_number is not None:
            key = "examine" + str(self.infobox_version_number)
            examine = self.extract_infobox_value(self.template, key)
        if examine is None:
            examine = self.extract_infobox_value(self.template, "examine")
        if examine is not None:
            self.item_dict["examine"] = infobox_cleaner.clean_examine(
                examine, self.item_dict["name"])
        else:
            # Being here means the extraction for "examine" failed
            key = "itemexamine" + str(self.infobox_version_number)
            examine = self.extract_infobox_value(self.template, key)
            if examine is None:
                examine = self.extract_infobox_value(self.template,
                                                     "itemexamine")
            if examine is not None:
                self.item_dict["examine"] = infobox_cleaner.clean_examine(
                    examine, self.item_dict["name"])
            else:
                self.item_dict["examine"] = None
                self.item_dict["incomplete"] = True

        # Determine if item has a buy limit
        if not self.item_dict["tradeable"]:
            self.item_dict["buy_limit"] = None
        else:
            try:
                self.item_dict["buy_limit"] = int(
                    self.buy_limits_data[self.item_dict["name"]])
                if self.item_dict["noted"]:
                    self.item_dict["buy_limit"] = None
            except KeyError:
                self.item_dict["buy_limit"] = None

        # We finished processing, set incomplete to false if not true
        if not self.item_dict.get("incomplete"):
            self.item_dict["incomplete"] = False

        return True

    def populate_equipable_properties_from_wiki_data(self) -> bool:
        """Parse the wiki text template and extract item bonus values from it."""
        # Initialize empty equipment dictionary
        self.item_dict["equipment"] = dict()

        # Extract the infobox bonuses template
        infobox_parser = WikitextTemplateParser(self.item_wikitext)
        has_infobox = infobox_parser.extract_infobox("infobox bonuses")
        if not has_infobox:
            has_infobox = infobox_parser.extract_infobox("infobox_bonuses")
            if not has_infobox:
                # No infobox bonuses found for the item!
                print(
                    "populate_equipable_properties: Item has no equipment infobox."
                )
                logging.critical(
                    "populate_equipable_properties: Item has no equipment infobox."
                )
                quit()

        # Set the template
        template = infobox_parser.template

        # STAGE ONE: EQUIPABLE ITEM

        # This item must be equipable by a player, set to True
        self.item_dict["equipable_by_player"] = True

        # Extract equipable item properties
        self.item_dict["equipment"]["attack_stab"] = self.clean_bonuses_value(
            template, "astab")
        self.item_dict["equipment"]["attack_slash"] = self.clean_bonuses_value(
            template, "aslash")
        self.item_dict["equipment"]["attack_crush"] = self.clean_bonuses_value(
            template, "acrush")
        self.item_dict["equipment"]["attack_magic"] = self.clean_bonuses_value(
            template, "amagic")
        self.item_dict["equipment"][
            "attack_ranged"] = self.clean_bonuses_value(template, "arange")
        self.item_dict["equipment"]["defence_stab"] = self.clean_bonuses_value(
            template, "dstab")
        self.item_dict["equipment"][
            "defence_slash"] = self.clean_bonuses_value(template, "dslash")
        self.item_dict["equipment"][
            "defence_crush"] = self.clean_bonuses_value(template, "dcrush")
        self.item_dict["equipment"][
            "defence_magic"] = self.clean_bonuses_value(template, "dmagic")
        self.item_dict["equipment"][
            "defence_ranged"] = self.clean_bonuses_value(template, "drange")
        self.item_dict["equipment"][
            "melee_strength"] = self.clean_bonuses_value(template, "str")
        self.item_dict["equipment"][
            "ranged_strength"] = self.clean_bonuses_value(template, "rstr")
        self.item_dict["equipment"]["magic_damage"] = self.clean_bonuses_value(
            template, "mdmg")
        self.item_dict["equipment"]["prayer"] = self.clean_bonuses_value(
            template, "prayer")

        # Determine the slot for the equipable item
        self.item_dict["equipment"]["slot"] = None
        try:
            self.item_dict["equipment"]["slot"] = self.strip_infobox(
                template.get("slot").value)
            self.item_dict["equipment"]["slot"] = self.item_dict["equipment"][
                "slot"].lower()
        except ValueError:
            self.item_dict["equipment"]["slot"] = None
            print(
                "populate_equipable_properties: Could not determine item slot..."
            )
            logging.critical(
                "populate_equipable_properties: Could not determine item slot..."
            )
            quit()

        # Determine the skill requirements for the equipable item
        self.item_dict["equipment"]["requirements"] = None
        try:
            requirements = self.skill_requirements[str(self.item_id)]
            self.item_dict["equipment"]["requirements"] = requirements
        except KeyError:
            self.item_dict["equipment"]["requirements"] = None

        # STAGE TWO: WEAPONS

        # If item is weapon, two-handed, or 2h, start processing the weapon data
        if (self.item_dict["equipment"]["slot"] == "weapon"
                or self.item_dict["equipment"]["slot"] == "two-handed"
                or self.item_dict["equipment"]["slot"] == "2h"):

            self.item_dict["weapon"] = dict()

            # Try set the attack speed of the weapon
            try:
                self.item_dict["weapon"]["attack_speed"] = int(
                    self.strip_infobox(template.get("aspeed").value))
            except ValueError:
                self.item_dict["weapon"]["attack_speed"] = None
                logging.critical(
                    "WEAPON: Could not determine weapon attack speed")

                # Item IDs with no known attack speed, set to zero
                if int(self.item_id) in [8871]:
                    self.item_dict["weapon"]["attack_speed"] = 0
                # Salamander fix, set to base attack speed of 5
                elif int(self.item_id) in [
                        10145, 10146, 10147, 10147, 10148, 10149
                ]:
                    self.item_dict["weapon"]["attack_speed"] = 5
                else:
                    pass
                    # quit()

            # Try to set the weapon type of the weapon
            try:
                weapon_type = self.weapon_types_data[str(
                    self.item_dict["id"])]["weapon_type"]
                self.item_dict["weapon"]["weapon_type"] = weapon_type
            except KeyError:
                self.item_dict["weapon"]["weapon_type"] = None
                print(
                    "populate_equipable_properties: Could not determine weapon type..."
                )
                logging.critical(
                    "populate_equipable_properties: Could not determine weapon type..."
                )
                quit()

            # Try to set stances available for the weapon
            try:
                self.item_dict["weapon"]["stances"] = self.weapon_stances_data[
                    self.item_dict["weapon"]["weapon_type"]]
            except KeyError:
                self.item_dict["weapon"]["stances"] = None
                print(
                    "populate_equipable_properties: Could not determine weapon stance..."
                )
                logging.critical(
                    "populate_equipable_properties: Could not determine weapon stance..."
                )
                quit()

            # Finally, set the equipable_weapon property to true
            self.item_dict["equipable_weapon"] = True

        else:
            # If the item is not a weapon, two-handed or 2h it is not a weapon
            self.item_dict["equipable_weapon"] = False

        return True

    def extract_infobox_value(
            self, template: mwparserfromhell.nodes.template.Template,
            key: str) -> str:
        """Helper method to extract a value from a template using a specified key.

        This helper method is a simple solution to repeatedly try to fetch a specific
        entry from a wiki text template (a mwparserfromhell template object).

        :param template: A mediawiki wiki text template.
        :param key: The key to query in the template.
        :return value: The extracted template value based on supplied key.
        """
        value = None
        try:
            value = template.get(key).value
            value = value.strip()
            return value
        except ValueError:
            return value

    def clean_bonuses_value(self,
                            template: mwparserfromhell.nodes.template.Template,
                            prop: str) -> int:
        """Clean a item bonuses value extracted from a wiki template.

        :param template: A mediawiki wiki text template.
        :param prop: The key to query in the template.
        :return value: The extracted template value that has been int cast.
        """
        value = None

        # Try and get the versioned infobox value
        if self.infobox_version_number is not None:
            key = prop + str(self.infobox_version_number)
            value = self.extract_infobox_value(template, key)

        # If unsuccessful, try and get the normal infoxbox value
        if value is None:
            value = self.extract_infobox_value(template, prop)

        if value is not None:
            value = self.strip_infobox(value)
            if isinstance(value, str):
                if value[0] == "-":
                    if value[1:].isdigit():
                        value = int(value)
                elif value[0] == "+":
                    if value[1:].isdigit():
                        value = int(value)
                else:
                    if value.isdigit():
                        value = int(value)
        else:
            value = 0

        return value

    def strip_infobox(self, value: str) -> str:
        """Generic infobox wiki text cleaner.

        :return clean_value: A cleaned wiki text value.
        """
        # Clean an passed InfoBox string
        clean_value = str(value)
        clean_value = clean_value.strip()
        clean_value = clean_value.replace("[", "")
        clean_value = clean_value.replace("]", "")
        return clean_value

    def compare_json_files(self, item_definition: ItemDefinition) -> bool:
        """Print the difference between this item and the database."""
        # Create JSON out object to compare
        current_json = item_definition.construct_json()

        # Try get existing entry (KeyError means it doesn't exist - aka a new item)
        try:
            existing_json = self.all_db_items[self.item_id]
        except KeyError:
            print(f">>> compare_json_files: NEW ITEM: {item_definition.id}")
            print(current_json)
            return

        if current_json == existing_json:
            return

        ddiff = DeepDiff(existing_json, current_json, ignore_order=True)
        logging.debug(
            f">>> compare_json_files: CHANGED ITEM: {item_definition.id}: {item_definition.name}, {item_definition.wiki_name}"
        )
        print(
            f">>> compare_json_files: CHANGED ITEM: {item_definition.id}: {item_definition.name}"
        )

        # try:
        #     added_properties = ddiff["dictionary_item_added"]
        #     print("   ", added_properties)
        # except KeyError:
        #     pass

        # try:
        #     changed_properties = ddiff["values_changed"]
        #     for k, v in changed_properties.items():
        #         print("   ", k, v["new_value"])
        # except KeyError:
        #     pass

        print(ddiff)
        return

    def validate_item(self):
        """Use the items-schema.json file to validate the populated item."""
        # Create JSON out object to validate
        current_json = self.item_definition.construct_json()

        # Open the JSON Schema for items
        path_to_schema = Path(config.TEST_PATH / "item_schema.json")
        with open(path_to_schema, 'r') as f:
            schema = json.loads(f.read())

        # Check the populate item object against the schema
        jsonschema.validate(instance=current_json, schema=schema)
Example #11
0
 def generate_item_object(self):
     """Generate the `ItemDefinition` object from the item_dict dictionary."""
     self.item_definition = ItemDefinition(**self.item_dict)