def test_uuid_creation(mock_card: Dict[str, Any], mock_file_info: Dict[str, Any]) -> None: """ Tests to ensure UUIDs don't regress :param mock_card: :param mock_file_info: :return: """ card = MTGJSONCard(mock_file_info["code"]) card.set_all(mock_card) uuid_new = card.get_uuid() assert uuid_new == "4b560297-2f1e-5f65-b118-289c21bdf887"
def test_uuid_creation( mock_card: Dict[str, Any], mock_file_info: Dict[str, Any] ) -> None: """ Tests to ensure UUIDs don't regress :param mock_card: :param mock_file_info: :return: """ card = MTGJSONCard(mock_file_info["code"]) card.set_all(mock_card) uuid_new = card.get_uuid() assert uuid_new == "4b560297-2f1e-5f65-b118-289c21bdf887"
def build_mtgjson_card(sf_card: Dict[str, Any], sf_card_face: int = 0) -> List[MTGJSONCard]: """ Build a mtgjson card (and all sub pieces of that card) :param sf_card: Card to build :param sf_card_face: Which part of the card (defaults to 0) :return: List of card(s) build (usually 1) """ mtgjson_cards: List[MTGJSONCard] = [] single_card = MTGJSONCard(sf_card["set"]) # Let us know what card we're trying to parse -- good for debugging :) LOGGER.info("Parsing {0} from {1}".format(sf_card.get("name"), sf_card.get("set"))) # If flip-type, go to card_faces for alt attributes face_data: Dict[str, Any] = sf_card if "card_faces" in sf_card: single_card.set_all({ "names": sf_card["name"].split(" // "), "scryfallId": sf_card["id"], "scryfallOracleId": sf_card["oracle_id"], "scryfallIllustrationId": sf_card.get("illustration_id"), }) face_data = sf_card["card_faces"][sf_card_face] # Split cards and rotational cards have this field, flip cards do not. # Remove rotational cards via the additional check if "mana_cost" in sf_card and "//" in sf_card["mana_cost"]: single_card.set( "colors", get_card_colors( sf_card["mana_cost"].split(" // ")[sf_card_face]), ) single_card.set( "faceConvertedManaCost", get_cmc( sf_card["mana_cost"].split("//")[sf_card_face].strip()), ) elif sf_card["layout"] in ["split", "transform", "aftermath"]: # Handle non-normal cards, as they'll a face split single_card.set( "faceConvertedManaCost", get_cmc(face_data.get("mana_cost", "0").strip()), ) # Watermark is only attributed on the front side, so we'll account for it single_card.set( "watermark", sf_card["card_faces"][0].get("watermark", None), single_card.clean_up_watermark, ) if sf_card["card_faces"][-1]["oracle_text"].startswith("Aftermath"): single_card.set("layout", "aftermath") single_card.set("artist", sf_card["card_faces"][sf_card_face].get("artist", "")) # Recursively parse the other cards within this card too # Only call recursive if it is the first time we see this card object if sf_card_face == 0: for i in range(1, len(sf_card["card_faces"])): LOGGER.info("Parsing additional card {0} face {1}".format( sf_card.get("name"), i)) mtgjson_cards += build_mtgjson_card(sf_card, i) else: single_card.set_all({ "scryfallId": sf_card.get("id"), "scryfallOracleId": sf_card["oracle_id"], "scryfallIllustrationId": sf_card.get("illustration_id"), }) # Characteristics that can are not shared to both sides of flip-type cards if face_data.get("mana_cost"): single_card.set("manaCost", face_data.get("mana_cost")) if "colors" not in single_card.keys(): if "colors" in face_data: single_card.set("colors", face_data.get("colors")) else: single_card.set("colors", sf_card.get("colors")) single_card.set_all({ "borderColor": sf_card.get("border_color"), "colorIdentity": sf_card.get("color_identity"), "convertedManaCost": sf_card.get("cmc"), "frameEffect": sf_card.get("frame_effect"), "frameVersion": sf_card.get("frame"), "hand": sf_card.get("hand_modifier"), "hasFoil": sf_card.get("foil"), "hasNonFoil": sf_card.get("nonfoil"), "isFullArt": sf_card.get("full_art"), "isOnlineOnly": sf_card.get("digital"), "isOversized": sf_card.get("oversized"), "isPromo": sf_card.get("promo"), "isReprint": sf_card.get("reprint"), "isReserved": sf_card.get("reserved"), "isStorySpotlight": sf_card.get("story_spotlight"), "isTextless": sf_card.get("textless"), "life": sf_card.get("life_modifier"), "loyalty": face_data.get("loyalty"), "name": face_data.get("name"), "number": sf_card.get("collector_number"), "power": face_data.get("power"), "tcgplayerProductId": sf_card.get("tcgplayer_id"), "text": face_data.get("oracle_text"), "toughness": face_data.get("toughness"), "type": face_data.get("type_line"), }) # Set MKM IDs if it exists if MKM_API.get(None): mkm_card_found = False for key, mkm_obj in MKM_SET_CARDS.get().items(): if single_card.get("name").lower() not in key: continue if "number" not in mkm_obj.keys() or ( mkm_obj.get("number") in single_card.get("number")): single_card.set_all({ "mcmId": mkm_obj["idProduct"], "mcmMetaId": mkm_obj["idMetaproduct"], }) single_card.set_mkm_url(mkm_obj["website"]) mkm_card_found = True break if not mkm_card_found: LOGGER.warning("Unable to find MKM information for #{} {}".format( single_card.get("number"), single_card.get("name"))) if "artist" not in single_card.keys(): single_card.set("artist", sf_card.get("artist")) if "layout" not in single_card.keys(): single_card.set("layout", sf_card.get("layout")) if "watermark" not in single_card.keys(): single_card.set( "watermark", face_data.get("watermark", None), single_card.clean_up_watermark, ) # "isPaper", "isMtgo", "isArena" for game_mode in sf_card.get("games", []): single_card.set("is{}".format(game_mode.capitalize()), True) if "flavor_text" in face_data: single_card.set("flavorText", face_data.get("flavor_text")) else: single_card.set("flavorText", sf_card.get("flavor_text")) if "color_indicator" in face_data: single_card.set("colorIndicator", face_data.get("color_indicator")) elif "color_indicator" in sf_card: single_card.set("colorIndicator", sf_card.get("color_indicator")) try: single_card.set("multiverseId", sf_card["multiverse_ids"][sf_card_face]) except IndexError: try: single_card.set("multiverseId", sf_card["multiverse_ids"][0]) except IndexError: single_card.set("multiverseId", None) # Add a "side" entry for split cards # Will only work for two faced cards (not meld, as they don't need this) if "names" in single_card.keys() and single_card.names_count(2): # chr(97) = 'a', chr(98) = 'b', ... single_card.set( "side", chr(single_card.get("names").index(single_card.get("name")) + 97)) # Characteristics that we have to format ourselves from provided data single_card.set( "isTimeshifted", (sf_card.get("frame") == "future") or (sf_card.get("set") == "tsb"), ) single_card.set("rarity", sf_card.get("rarity")) # Characteristics that we need custom functions to parse print_search_url: str = sf_card["prints_search_uri"].replace("%22", "") single_card.set("legalities", scryfall.parse_legalities(sf_card["legalities"])) single_card.set( "rulings", sorted( scryfall.parse_rulings(sf_card["rulings_uri"]), key=lambda ruling: ruling["date"], ), ) single_card.set("printings", sorted(scryfall.parse_printings(print_search_url))) card_types: Tuple[List[str], List[str], List[str]] = scryfall.parse_card_types( single_card.get("type")) single_card.set("supertypes", card_types[0]) single_card.set("types", card_types[1]) single_card.set("subtypes", card_types[2]) # Handle meld and all parts tokens issues # Will re-address naming if a split card already if "all_parts" in sf_card: meld_holder = [] single_card.set("names", []) for a_part in sf_card["all_parts"]: if a_part["component"] != "token": if "//" in a_part.get("name"): single_card.set("names", a_part.get("name").split(" // ")) break # This is a meld only-fix, so we ignore tokens/combo pieces if "meld" in a_part["component"]: meld_holder.append(a_part["component"]) single_card.append("names", a_part.get("name")) # If the only entry is the original card, empty the names array if single_card.names_count(1) and single_card.get( "name") in single_card.get("names"): single_card.remove("names") # Meld cards should be CardA, Meld, CardB. This fixes that via swap # meld_holder if meld_holder and meld_holder[1] != "meld_result": single_card.get("names")[1], single_card.get("names")[2] = ( single_card.get("names")[2], single_card.get("names")[1], ) # Since we built meld cards later, we will add the "side" attribute now if single_card.names_count(3): # MELD if single_card.get("name") == single_card.get("names")[0]: single_card.set("side", "a") elif single_card.get("name") == single_card.get("names")[2]: single_card.set("side", "b") else: single_card.set("side", "c") # Characteristics that we cannot get from Scryfall # Characteristics we have to do further API calls for single_card.set( "foreignData", scryfall.parse_foreign( print_search_url, single_card.get("name"), single_card.get("number"), sf_card["set"], ), ) if single_card.get("multiverseId") is not None: gatherer_cards = gatherer.get_cards(single_card.get("multiverseId"), single_card.set_code) try: gatherer_card = gatherer_cards[sf_card_face] single_card.set("originalType", gatherer_card.original_types) single_card.set("originalText", gatherer_card.original_text) except IndexError: LOGGER.warning("Unable to parse originals for {}".format( single_card.get("name"))) mtgjson_cards.append(single_card) return mtgjson_cards
def build_mtgjson_tokens(sf_tokens: List[Dict[str, Any]], sf_card_face: int = 0) -> List[MTGJSONCard]: """ Convert Scryfall tokens to MTGJSON tokens :param sf_tokens: All tokens in a set :param sf_card_face: Faces of the token index :return: List of MTGJSON tokens """ token_cards: List[MTGJSONCard] = [] for sf_token in sf_tokens: token_card = MTGJSONCard(sf_token["set"]) if "card_faces" in sf_token: token_card.set("names", sf_token["name"].split(" // ")) face_data = sf_token["card_faces"][sf_card_face] # Prevent duplicate UUIDs for split card halves # Remove the last character and replace with the id of the card face token_card.set("scryfallId", sf_token["id"]) token_card.set("scryfallOracleId", sf_token["oracle_id"]) token_card.set("scryfallIllustrationId", sf_token.get("illustration_id")) # Recursively parse the other cards within this card too # Only call recursive if it is the first time we see this card object if sf_card_face == 0: for i in range(1, len(sf_token["card_faces"])): LOGGER.info("Parsing additional card {0} face {1}".format( sf_token.get("name"), i)) token_cards += build_mtgjson_tokens([sf_token], i) if "id" not in sf_token.keys(): LOGGER.info( "Scryfall_ID not found in {}. Discarding {}".format( sf_token.get("name"), sf_token)) continue token_card.set_all({ "name": face_data.get("name"), "type": face_data.get("type_line"), "text": face_data.get("oracle_text"), "power": face_data.get("power"), "colors": face_data.get("colors"), "colorIdentity": sf_token.get("color_identity"), "toughness": face_data.get("toughness"), "loyalty": face_data.get("loyalty"), "watermark": sf_token.get("watermark"), "scryfallId": sf_token["id"], "scryfallOracleId": sf_token.get("oracle_id"), "scryfallIllustrationId": sf_token.get("illustration_id"), "layout": "double_faced_token", "side": chr(97 + sf_card_face), "borderColor": face_data.get("border_color"), "artist": face_data.get("artist"), "isOnlineOnly": sf_token.get("digital"), "number": sf_token.get("collector_number"), }) else: token_card.set_all({ "name": sf_token.get("name"), "type": sf_token.get("type_line"), "text": sf_token.get("oracle_text"), "power": sf_token.get("power"), "colors": sf_token.get("colors"), "colorIdentity": sf_token.get("color_identity"), "toughness": sf_token.get("toughness"), "loyalty": sf_token.get("loyalty"), "watermark": sf_token.get("watermark"), "scryfallId": sf_token["id"], "scryfallOracleId": sf_token.get("oracle_id"), "scryfallIllustrationId": sf_token.get("illustration_id"), "borderColor": sf_token.get("border_color"), "artist": sf_token.get("artist"), "isOnlineOnly": sf_token.get("digital"), "number": sf_token.get("collector_number"), }) if sf_token.get("layout") == "token": token_card.set("layout", "normal") else: token_card.set("layout", sf_token.get("layout")) reverse_related: List[str] = [] if "all_parts" in sf_token: for a_part in sf_token["all_parts"]: if a_part.get("name") != token_card.get("name"): reverse_related.append(a_part.get("name")) token_card.set("reverseRelated", reverse_related) LOGGER.info("Parsed {0} from {1}".format(token_card.get("name"), sf_token.get("set"))) token_cards.append(token_card) return token_cards
def build_mtgjson_card( sf_card: Dict[str, Any], sf_card_face: int = 0 ) -> List[MTGJSONCard]: """ Build a mtgjson card (and all sub pieces of that card) :param sf_card: Card to build :param sf_card_face: Which part of the card (defaults to 0) :return: List of card(s) build (usually 1) """ mtgjson_cards: List[MTGJSONCard] = [] single_card = MTGJSONCard(sf_card["set"]) # Let us know what card we're trying to parse -- good for debugging :) LOGGER.info("Parsing {0} from {1}".format(sf_card.get("name"), sf_card.get("set"))) # If flip-type, go to card_faces for alt attributes face_data: Dict[str, Any] = sf_card if "card_faces" in sf_card: single_card.set_all( { "names": sf_card["name"].split(" // "), "scryfallId": sf_card["id"], "scryfallOracleId": sf_card["oracle_id"], "scryfallIllustrationId": sf_card.get("illustration_id"), } ) face_data = sf_card["card_faces"][sf_card_face] # Split cards and rotational cards have this field, flip cards do not. # Remove rotational cards via the additional check if "mana_cost" in sf_card and "//" in sf_card["mana_cost"]: single_card.set( "colors", get_card_colors(sf_card["mana_cost"].split(" // ")[sf_card_face]), ) single_card.set( "faceConvertedManaCost", get_cmc(sf_card["mana_cost"].split("//")[sf_card_face].strip()), ) elif sf_card["layout"] in ["split", "transform", "aftermath"]: # Handle non-normal cards, as they'll a face split single_card.set( "faceConvertedManaCost", get_cmc(face_data.get("mana_cost", "0").strip()), ) # Watermark is only attributed on the front side, so we'll account for it single_card.set( "watermark", sf_card["card_faces"][0].get("watermark", None), single_card.clean_up_watermark, ) if sf_card["card_faces"][-1]["oracle_text"].startswith("Aftermath"): single_card.set("layout", "aftermath") single_card.set("artist", sf_card["card_faces"][sf_card_face].get("artist", "")) # Recursively parse the other cards within this card too # Only call recursive if it is the first time we see this card object if sf_card_face == 0: for i in range(1, len(sf_card["card_faces"])): LOGGER.info( "Parsing additional card {0} face {1}".format( sf_card.get("name"), i ) ) mtgjson_cards += build_mtgjson_card(sf_card, i) else: single_card.set_all( { "scryfallId": sf_card.get("id"), "scryfallOracleId": sf_card["oracle_id"], "scryfallIllustrationId": sf_card.get("illustration_id"), } ) # Characteristics that can are not shared to both sides of flip-type cards if face_data.get("mana_cost"): single_card.set("manaCost", face_data.get("mana_cost")) if "colors" not in single_card.keys(): if "colors" in face_data: single_card.set("colors", face_data.get("colors")) else: single_card.set("colors", sf_card.get("colors")) single_card.set_all( { "name": face_data.get("name"), "type": face_data.get("type_line"), "text": face_data.get("oracle_text"), "power": face_data.get("power"), "toughness": face_data.get("toughness"), "loyalty": face_data.get("loyalty"), "borderColor": sf_card.get("border_color"), "colorIdentity": sf_card.get("color_identity"), "frameVersion": sf_card.get("frame"), "hasFoil": sf_card.get("foil"), "hasNonFoil": sf_card.get("nonfoil"), "isOnlineOnly": sf_card.get("digital"), "isOversized": sf_card.get("oversized"), "number": sf_card.get("collector_number"), "isReserved": sf_card.get("reserved"), "frameEffect": sf_card.get("frame_effect"), "tcgplayerProductId": sf_card.get("tcgplayer_id"), "life": sf_card.get("life_modifier"), "hand": sf_card.get("hand_modifier"), "convertedManaCost": sf_card.get("cmc"), } ) # Set MKM IDs if it exists if MKM_API.get(None): mkm_card_found = False for key, mkm_obj in MKM_SET_CARDS.get().items(): if single_card.get("name").lower() not in key: continue if "number" not in mkm_obj.keys() or ( mkm_obj.get("number") in single_card.get("number") ): single_card.set_all( { "mcmId": mkm_obj["idProduct"], "mcmMetaId": mkm_obj["idMetaproduct"], } ) single_card.set_mkm_url(mkm_obj["website"]) mkm_card_found = True break if not mkm_card_found: LOGGER.warning( "Unable to find MKM information for #{} {}".format( single_card.get("number"), single_card.get("name") ) ) if "artist" not in single_card.keys(): single_card.set("artist", sf_card.get("artist")) if "layout" not in single_card.keys(): single_card.set("layout", sf_card.get("layout")) if "watermark" not in single_card.keys(): single_card.set( "watermark", face_data.get("watermark", None), single_card.clean_up_watermark, ) if "flavor_text" in face_data: single_card.set("flavorText", face_data.get("flavor_text")) else: single_card.set("flavorText", sf_card.get("flavor_text")) if "color_indicator" in face_data: single_card.set("colorIndicator", face_data.get("color_indicator")) elif "color_indicator" in sf_card: single_card.set("colorIndicator", sf_card.get("color_indicator")) try: single_card.set("multiverseId", sf_card["multiverse_ids"][sf_card_face]) except IndexError: try: single_card.set("multiverseId", sf_card["multiverse_ids"][0]) except IndexError: single_card.set("multiverseId", None) # Add a "side" entry for split cards # Will only work for two faced cards (not meld, as they don't need this) if "names" in single_card.keys() and single_card.names_count(2): # chr(97) = 'a', chr(98) = 'b', ... single_card.set( "side", chr(single_card.get("names").index(single_card.get("name")) + 97) ) # Characteristics that we have to format ourselves from provided data single_card.set( "isTimeshifted", (sf_card.get("frame") == "future") or (sf_card.get("set") == "tsb"), ) single_card.set("rarity", sf_card.get("rarity")) # Characteristics that we need custom functions to parse print_search_url: str = sf_card["prints_search_uri"].replace("%22", "") single_card.set("legalities", scryfall.parse_legalities(sf_card["legalities"])) single_card.set( "rulings", sorted( scryfall.parse_rulings(sf_card["rulings_uri"]), key=lambda ruling: ruling["date"], ), ) single_card.set("printings", sorted(scryfall.parse_printings(print_search_url))) card_types: Tuple[List[str], List[str], List[str]] = scryfall.parse_card_types( single_card.get("type") ) single_card.set("supertypes", card_types[0]) single_card.set("types", card_types[1]) single_card.set("subtypes", card_types[2]) # Handle meld and all parts tokens issues # Will re-address naming if a split card already if "all_parts" in sf_card: meld_holder = [] single_card.set("names", []) for a_part in sf_card["all_parts"]: if a_part["component"] != "token": if "//" in a_part.get("name"): single_card.set("names", a_part.get("name").split(" // ")) break # This is a meld only-fix, so we ignore tokens/combo pieces if "meld" in a_part["component"]: meld_holder.append(a_part["component"]) single_card.append("names", a_part.get("name")) # If the only entry is the original card, empty the names array if single_card.names_count(1) and single_card.get("name") in single_card.get( "names" ): single_card.remove("names") # Meld cards should be CardA, Meld, CardB. This fixes that via swap # meld_holder if meld_holder and meld_holder[1] != "meld_result": single_card.get("names")[1], single_card.get("names")[2] = ( single_card.get("names")[2], single_card.get("names")[1], ) # Since we built meld cards later, we will add the "side" attribute now if single_card.names_count(3): # MELD if single_card.get("name") == single_card.get("names")[0]: single_card.set("side", "a") elif single_card.get("name") == single_card.get("names")[2]: single_card.set("side", "b") else: single_card.set("side", "c") # Characteristics that we cannot get from Scryfall # Characteristics we have to do further API calls for single_card.set( "foreignData", scryfall.parse_foreign( print_search_url, single_card.get("name"), single_card.get("number"), sf_card["set"], ), ) if single_card.get("multiverseId") is not None: gatherer_cards = gatherer.get_cards(single_card.get("multiverseId")) try: gatherer_card = gatherer_cards[sf_card_face] single_card.set("originalType", gatherer_card.original_types) single_card.set("originalText", gatherer_card.original_text) except IndexError: LOGGER.warning( "Unable to parse originals for {}".format(single_card.get("name")) ) mtgjson_cards.append(single_card) return mtgjson_cards
def build_mtgjson_tokens( sf_tokens: List[Dict[str, Any]], sf_card_face: int = 0 ) -> List[MTGJSONCard]: """ Convert Scryfall tokens to MTGJSON tokens :param sf_tokens: All tokens in a set :param sf_card_face: Faces of the token index :return: List of MTGJSON tokens """ token_cards: List[MTGJSONCard] = [] for sf_token in sf_tokens: token_card = MTGJSONCard(sf_token["set"]) if "card_faces" in sf_token: token_card.set("names", sf_token["name"].split(" // ")) face_data = sf_token["card_faces"][sf_card_face] # Prevent duplicate UUIDs for split card halves # Remove the last character and replace with the id of the card face token_card.set("scryfallId", sf_token["id"]) token_card.set("scryfallOracleId", sf_token["oracle_id"]) token_card.set("scryfallIllustrationId", sf_token.get("illustration_id")) # Recursively parse the other cards within this card too # Only call recursive if it is the first time we see this card object if sf_card_face == 0: for i in range(1, len(sf_token["card_faces"])): LOGGER.info( "Parsing additional card {0} face {1}".format( sf_token.get("name"), i ) ) token_cards += build_mtgjson_tokens([sf_token], i) if "id" not in sf_token.keys(): LOGGER.info( "Scryfall_ID not found in {}. Discarding {}".format( sf_token.get("name"), sf_token ) ) continue token_card.set_all( { "name": face_data.get("name"), "type": face_data.get("type_line"), "text": face_data.get("oracle_text"), "power": face_data.get("power"), "colors": face_data.get("colors"), "colorIdentity": sf_token.get("color_identity"), "toughness": face_data.get("toughness"), "loyalty": face_data.get("loyalty"), "watermark": sf_token.get("watermark"), "scryfallId": sf_token["id"], "scryfallOracleId": sf_token.get("oracle_id"), "scryfallIllustrationId": sf_token.get("illustration_id"), "layout": "double_faced_token", "side": chr(97 + sf_card_face), "borderColor": face_data.get("border_color"), "artist": face_data.get("artist"), "isOnlineOnly": sf_token.get("digital"), "number": sf_token.get("collector_number"), } ) else: token_card.set_all( { "name": sf_token.get("name"), "type": sf_token.get("type_line"), "text": sf_token.get("oracle_text"), "power": sf_token.get("power"), "colors": sf_token.get("colors"), "colorIdentity": sf_token.get("color_identity"), "toughness": sf_token.get("toughness"), "loyalty": sf_token.get("loyalty"), "layout": "normal", "watermark": sf_token.get("watermark"), "scryfallId": sf_token["id"], "scryfallOracleId": sf_token.get("oracle_id"), "scryfallIllustrationId": sf_token.get("illustration_id"), "borderColor": sf_token.get("border_color"), "artist": sf_token.get("artist"), "isOnlineOnly": sf_token.get("digital"), "number": sf_token.get("collector_number"), } ) reverse_related: List[str] = [] if "all_parts" in sf_token: for a_part in sf_token["all_parts"]: if a_part.get("name") != token_card.get("name"): reverse_related.append(a_part.get("name")) token_card.set("reverseRelated", reverse_related) LOGGER.info( "Parsed {0} from {1}".format(token_card.get("name"), sf_token.get("set")) ) token_cards.append(token_card) return token_cards