Mark's Changes for SQL (#29)

* Added new fields.

- Added `hasLeadershipSkills` and `hasNoDeckLimit`

* Update json2sql.py

* Changed for new build

* Export .sql file

* fixes
This commit is contained in:
Zach H 2019-09-30 00:23:06 -04:00 committed by GitHub
parent 0d08bb8b92
commit 9b18d52a5d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -6,9 +6,11 @@ import json
import logging
import pathlib
import sqlite3
import time
from typing import Any, Dict, List, Union
LOGGER = logging.getLogger(__name__)
version = "v4.5.x" # need to automate this
def main() -> None:
@ -24,7 +26,7 @@ def main() -> None:
)
parser.add_argument(
"-o",
help="output file (*.sqlite, *.db, *.sqlite3, *.db3)",
help="output file (*.sqlite, *.db, *.sqlite3, *.db3, *.sql)",
required=True,
metavar="fileOut",
)
@ -32,20 +34,37 @@ def main() -> None:
# Define our I/O paths
input_file = pathlib.Path(args.i).expanduser()
output_file = pathlib.Path(args.o).expanduser()
output_file = {"path": pathlib.Path(args.o).expanduser(), "handle": None}
if not validate_io_streams(input_file, output_file):
exit(1)
# Build the SQLite database
sql_connection = sqlite3.connect(str(output_file))
sql_connection.execute("pragma journal_mode=wal;")
# Build the SQLite database/file
if output_file["path"].suffix == ".sql":
output_file["handle"] = open(output_file["path"], "w", encoding="utf8")
output_file["handle"].write(
"\n".join(
(
"-- MTGSQLive Output File",
"-- ({})".format(str(time.strftime("%Y-%m-%d %H:%M:%S"))),
"-- MTGJSON Version: {}".format(version),
"",
"BEGIN TRANSACTION;",
"",
"",
)
)
)
else:
output_file["handle"] = sqlite3.connect(str(output_file["path"]))
output_file["handle"].execute("pragma journal_mode=wal;")
build_sql_schema(sql_connection)
parse_and_import_cards(input_file, sql_connection)
build_sql_schema(output_file)
parse_and_import_cards(input_file, output_file)
output_file["handle"].close()
def validate_io_streams(input_file: pathlib.Path, output_file: pathlib.Path) -> bool:
def validate_io_streams(input_file: pathlib.Path, output_file: Dict) -> bool:
"""
Ensure I/O paths are valid and clean for program
:param input_file: Input file (JSON)
@ -61,222 +80,232 @@ def validate_io_streams(input_file: pathlib.Path, output_file: pathlib.Path) ->
LOGGER.fatal(f"Invalid input file/directory. ({input_file})")
return False
output_file.parent.mkdir(exist_ok=True)
if output_file.is_file():
LOGGER.warning(f"Output file {output_file} exists already, moving it.")
output_file.replace(output_file.parent.joinpath(output_file.name + ".old"))
output_file["path"].parent.mkdir(exist_ok=True)
if output_file["path"].is_file():
LOGGER.warning(f"Output file {output_file['path']} exists already, moving it.")
output_file["path"].replace(
output_file["path"].parent.joinpath(output_file["path"].name + ".old")
)
return True
def build_sql_schema(sql_connection: sqlite3.Connection) -> None:
def build_sql_schema(output_file: Dict) -> None:
"""
Create the SQLite DB schema
:param sql_connection: Connection to the database
:param output_file: Output info dict
"""
LOGGER.info("Building SQLite Schema")
cursor = sql_connection.cursor()
# Build Set table
cursor.execute(
"CREATE TABLE `sets` ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"baseSetSize INTEGER,"
"block TEXT,"
"boosterV3 TEXT,"
"code TEXT UNIQUE NOT NULL,"
"codeV3 TEXT,"
"isFoilOnly INTEGER NOT NULL DEFAULT 0," # boolean
"isForeignOnly INTEGER NOT NULL DEFAULT 0," # boolean
"isOnlineOnly INTEGER NOT NULL DEFAULT 0," # boolean
"isPartialPreview INTEGER NOT NULL DEFAULT 0," # boolean
"keyruneCode TEXT,"
"mcmId INTEGER,"
"mcmName TEXT,"
"meta TEXT,"
"mtgoCode TEXT,"
"name TEXT,"
"parentCode TEXT,"
"releaseDate TEXT,"
"tcgplayerGroupId INTEGER,"
"totalSetSize INTEGER,"
"type TEXT"
")"
)
# Build cards table
cursor.execute(
"CREATE TABLE `cards` ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"artist TEXT,"
"borderColor TEXT,"
"colorIdentity TEXT,"
"colorIndicator TEXT,"
"colors TEXT,"
"convertedManaCost FLOAT,"
"duelDeck TEXT(1),"
"edhrecRank TEXT,"
"faceConvertedManaCost FLOAT,"
"flavorText TEXT,"
"frameEffect TEXT,"
"frameVersion TEXT,"
"hand TEXT,"
"hasFoil INTEGER NOT NULL DEFAULT 0," # boolean
"hasNoDeckLimit INTEGER NOT NULL DEFAULT 0," # boolean
"hasNonFoil INTEGER NOT NULL DEFAULT 0," # boolean
"isAlternative INTEGER NOT NULL DEFAULT 0," # boolean
"isArena INTEGER NOT NULL DEFAULT 0," # boolean
"isFullArt INTEGER NOT NULL DEFAULT 0," # boolean
"isMtgo INTEGER NOT NULL DEFAULT 0," # boolean
"isOnlineOnly INTEGER NOT NULL DEFAULT 0," # boolean
"isOversized INTEGER NOT NULL DEFAULT 0," # boolean
"isPaper INTEGER NOT NULL DEFAULT 0," # boolean
"isPromo INTEGER NOT NULL DEFAULT 0," # boolean
"isReprint INTEGER NOT NULL DEFAULT 0," # boolean
"isReserved INTEGER NOT NULL DEFAULT 0," # boolean
"isStarter INTEGER NOT NULL DEFAULT 0," # boolean
"isStorySpotlight INTEGER NOT NULL DEFAULT 0," # boolean
"isTextless INTEGER NOT NULL DEFAULT 0," # boolean
"isTimeshifted INTEGER NOT NULL DEFAULT 0," # boolean
"layout TEXT,"
"leadershipSkills TEXT,"
"life TEXT,"
"loyalty TEXT,"
"manaCost TEXT,"
"mcmId INTEGER,"
"mcmMetaId INTEGER,"
"mcmName TEXT,"
"mtgArenaId INTEGER,"
"mtgoFoilId INTEGER,"
"mtgoId INTEGER,"
"mtgstocksId INTEGER,"
"multiverseId INTEGER,"
"name TEXT,"
"names TEXT,"
"number TEXT,"
"originalText TEXT,"
"originalType TEXT,"
"power TEXT,"
"printings TEXT,"
"purchaseUrls TEXT,"
"rarity TEXT,"
"scryfallId TEXT(36),"
"scryfallIllustrationId TEXT(36),"
"scryfallOracleId TEXT(36),"
"setCode TEXT REFERENCES sets(code) ON UPDATE CASCADE ON DELETE CASCADE,"
"side TEXT,"
"subtypes TEXT,"
"supertypes TEXT,"
"tcgplayerProductId INTEGER,"
"tcgplayerPurchaseUrl TEXT,"
"text TEXT,"
"toughness TEXT,"
"type TEXT,"
"types TEXT,"
"uuid TEXT(36) UNIQUE NOT NULL,"
"variations TEXT,"
"watermark TEXT"
")"
#"CREATE UNIQUE INDEX 'cards_uuid' ON cards(uuid);"
)
# Build tokens table
cursor.execute(
"CREATE TABLE `tokens` ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"artist TEXT,"
"borderColor TEXT,"
"colorIdentity TEXT,"
"colorIndicator TEXT,"
"colors TEXT,"
"duelDeck TEXT(1),"
"isOnlineOnly INTEGER NOT NULL DEFAULT 0," # boolean
"layout TEXT,"
"loyalty TEXT,"
"name TEXT,"
"names TEXT,"
"number TEXT,"
"power TEXT,"
"reverseRelated TEXT,"
"scryfallId TEXT(36),"
"scryfallIllustrationId TEXT(36),"
"scryfallOracleId TEXT(36),"
"setCode TEXT REFERENCES sets(code) ON UPDATE CASCADE ON DELETE CASCADE,"
"side TEXT,"
"text TEXT,"
"toughness TEXT,"
"type TEXT,"
"uuid TEXT(36) UNIQUE,"
"watermark TEXT"
")"
)
# Translations for set names
cursor.execute(
"CREATE TABLE `set_translations` ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"language TEXT,"
"setCode TEXT REFERENCES sets(code) ON UPDATE CASCADE ON DELETE CASCADE,"
"translation TEXT"
")"
)
# Build foreignData table
cursor.execute(
"CREATE TABLE `foreignData` ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"flavorText TEXT,"
"language TEXT,"
"multiverseId INTEGER,"
"name TEXT,"
"text TEXT,"
"type TEXT,"
"uuid TEXT(36) REFERENCES cards(uuid) ON UPDATE CASCADE ON DELETE CASCADE"
")"
)
# Build legalities table
cursor.execute(
"CREATE TABLE `legalities` ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"format TEXT,"
"status TEXT,"
"uuid TEXT(36) REFERENCES cards(uuid) ON UPDATE CASCADE ON DELETE CASCADE"
")"
)
# Build ruling table
cursor.execute(
"CREATE TABLE `rulings` ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"date TEXT,"
"text TEXT,"
"uuid TEXT(36) REFERENCES cards(uuid) ON UPDATE CASCADE ON DELETE CASCADE"
")"
)
# Build prices table
cursor.execute(
"CREATE TABLE `prices` ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"date TEXT,"
"price REAL,"
"type TEXT,"
"uuid TEXT(36) REFERENCES cards(uuid) ON UPDATE CASCADE ON DELETE CASCADE"
")"
)
schema = {
"sets": [
"CREATE TABLE `sets` (",
"id INTEGER PRIMARY KEY AUTOINCREMENT,",
"baseSetSize INTEGER,",
"block TEXT,",
"boosterV3 TEXT,",
"code TEXT UNIQUE NOT NULL,",
"codeV3 TEXT,",
"isFoilOnly INTEGER NOT NULL DEFAULT 0,", # boolean
"isForeignOnly INTEGER NOT NULL DEFAULT 0,", # boolean
"isOnlineOnly INTEGER NOT NULL DEFAULT 0,", # boolean
"isPartialPreview INTEGER NOT NULL DEFAULT 0,", # boolean
"keyruneCode TEXT,",
"mcmId INTEGER,",
"mcmName TEXT,",
"meta TEXT,",
"mtgoCode TEXT,",
"name TEXT,",
"parentCode TEXT,",
"releaseDate TEXT,",
"tcgplayerGroupId INTEGER,",
"totalSetSize INTEGER,",
"type TEXT",
");",
"",
"",
],
"cards": [
"CREATE TABLE `cards` (",
"id INTEGER PRIMARY KEY AUTOINCREMENT,",
"artist TEXT,",
"borderColor TEXT,",
"colorIdentity TEXT,",
"colorIndicator TEXT,",
"colors TEXT,",
"convertedManaCost FLOAT,",
"duelDeck TEXT(1),",
"edhrecRank TEXT,",
"faceConvertedManaCost FLOAT,",
"flavorText TEXT,",
"frameEffect TEXT,",
"frameVersion TEXT,",
"hand TEXT,",
"hasFoil INTEGER NOT NULL DEFAULT 0,", # boolean
"hasNoDeckLimit INTEGER NOT NULL DEFAULT 0,", # boolean
"hasNonFoil INTEGER NOT NULL DEFAULT 0,", # boolean
"isAlternative INTEGER NOT NULL DEFAULT 0,", # boolean
"isArena INTEGER NOT NULL DEFAULT 0,", # boolean
"isFullArt INTEGER NOT NULL DEFAULT 0,", # boolean
"isMtgo INTEGER NOT NULL DEFAULT 0,", # boolean
"isOnlineOnly INTEGER NOT NULL DEFAULT 0,", # boolean
"isOversized INTEGER NOT NULL DEFAULT 0,", # boolean
"isPaper INTEGER NOT NULL DEFAULT 0,", # boolean
"isPromo INTEGER NOT NULL DEFAULT 0,", # boolean
"isReprint INTEGER NOT NULL DEFAULT 0,", # boolean
"isReserved INTEGER NOT NULL DEFAULT 0,", # boolean
"isStarter INTEGER NOT NULL DEFAULT 0,", # boolean
"isStorySpotlight INTEGER NOT NULL DEFAULT 0,", # boolean
"isTextless INTEGER NOT NULL DEFAULT 0,", # boolean
"isTimeshifted INTEGER NOT NULL DEFAULT 0,", # boolean
"layout TEXT,",
"leadershipSkills TEXT,",
"life TEXT,",
"loyalty TEXT,",
"manaCost TEXT,",
"mcmId INTEGER,",
"mcmMetaId INTEGER,",
"mcmName TEXT,",
"mtgArenaId INTEGER,",
"mtgoFoilId INTEGER,",
"mtgoId INTEGER,",
"mtgstocksId INTEGER,",
"multiverseId INTEGER,",
"name TEXT,",
"names TEXT,",
"number TEXT,",
"originalText TEXT,",
"originalType TEXT,",
"power TEXT,",
"printings TEXT,",
"purchaseUrls TEXT,",
"rarity TEXT,",
"scryfallId TEXT(36),",
"scryfallIllustrationId TEXT(36),",
"scryfallOracleId TEXT(36),",
"setCode TEXT REFERENCES sets(code) ON UPDATE CASCADE ON DELETE CASCADE,",
"side TEXT,",
"subtypes TEXT,",
"supertypes TEXT,",
"tcgplayerProductId INTEGER,",
"tcgplayerPurchaseUrl TEXT,",
"text TEXT,",
"toughness TEXT,",
"type TEXT,",
"types TEXT,",
"uuid TEXT(36) UNIQUE NOT NULL,",
"variations TEXT,",
"watermark TEXT",
");",
# "CREATE UNIQUE INDEX 'cards_uuid' ON cards(uuid);"
"",
"",
],
"tokens": [
"CREATE TABLE `tokens` (",
"id INTEGER PRIMARY KEY AUTOINCREMENT,",
"artist TEXT,",
"borderColor TEXT,",
"colorIdentity TEXT,",
"colorIndicator TEXT,",
"colors TEXT,",
"duelDeck TEXT(1),",
"isOnlineOnly INTEGER NOT NULL DEFAULT 0,", # boolean
"layout TEXT,",
"loyalty TEXT,",
"name TEXT,",
"names TEXT,",
"number TEXT,",
"power TEXT,",
"reverseRelated TEXT,",
"scryfallId TEXT(36),",
"scryfallIllustrationId TEXT(36),",
"scryfallOracleId TEXT(36),",
"setCode TEXT REFERENCES sets(code) ON UPDATE CASCADE ON DELETE CASCADE,",
"side TEXT,",
"text TEXT,",
"toughness TEXT,",
"type TEXT,",
"uuid TEXT(36) UNIQUE,",
"watermark TEXT",
");",
"",
"",
],
"set_translations": [
"CREATE TABLE `set_translations` (",
"id INTEGER PRIMARY KEY AUTOINCREMENT,",
"language TEXT,",
"setCode TEXT REFERENCES sets(code) ON UPDATE CASCADE ON DELETE CASCADE,",
"translation TEXT",
");",
"",
"",
],
"foreignData": [
"CREATE TABLE `foreignData` (",
"id INTEGER PRIMARY KEY AUTOINCREMENT,",
"flavorText TEXT,",
"language TEXT,",
"multiverseId INTEGER,",
"name TEXT,",
"text TEXT,",
"type TEXT,",
"uuid TEXT(36) REFERENCES cards(uuid) ON UPDATE CASCADE ON DELETE CASCADE",
");",
"",
"",
],
"legalities": [
"CREATE TABLE `legalities` (",
"id INTEGER PRIMARY KEY AUTOINCREMENT,",
"format TEXT,",
"status TEXT,",
"uuid TEXT(36) REFERENCES cards(uuid) ON UPDATE CASCADE ON DELETE CASCADE",
");",
"",
"",
],
"rulings": [
"CREATE TABLE `rulings` (",
"id INTEGER PRIMARY KEY AUTOINCREMENT,",
"date TEXT,",
"text TEXT,",
"uuid TEXT(36) REFERENCES cards(uuid) ON UPDATE CASCADE ON DELETE CASCADE",
");",
"",
"",
],
"prices": [
"CREATE TABLE `prices` (",
"id INTEGER PRIMARY KEY AUTOINCREMENT,",
"date TEXT,",
"price REAL,",
"type TEXT,",
"uuid TEXT(36) REFERENCES cards(uuid) ON UPDATE CASCADE ON DELETE CASCADE",
");",
"",
"",
],
}
# Execute the commands
sql_connection.commit()
if output_file["path"].suffix == ".sql":
for q in schema.values():
output_file["handle"].write("\n".join(q))
output_file["handle"].write("COMMIT;\n\nBEGIN TRANSACTION;\n")
else:
cursor = output_file["handle"].cursor()
for q in schema.values():
cursor.execute("".join(q))
output_file["handle"].commit()
def parse_and_import_cards(
input_file: pathlib.Path, sql_connection: sqlite3.Connection
) -> None:
def parse_and_import_cards(input_file: pathlib.Path, output_file: Dict) -> None:
"""
Parse the JSON cards and input them into the database
:param input_file: AllSets.json file
:param sql_connection: Database connection
:param output_file: Uutput info dictionary
"""
if input_file.is_file():
LOGGER.info("Loading JSON into memory")
@ -287,26 +316,24 @@ def parse_and_import_cards(
# Handle set insertion
LOGGER.info("Inserting set row for {}".format(set_code))
set_insert_values = handle_set_row_insertion(set_data)
sql_dict_insert(set_insert_values, "sets", sql_connection)
sql_dict_insert(set_insert_values, "sets", output_file)
for card in set_data.get("cards"):
LOGGER.debug("Inserting card row for {}".format(card.get("name")))
card_attr: Dict[str, Any] = handle_card_row_insertion(card, set_code)
sql_insert_all_card_fields(card_attr, sql_connection)
sql_insert_all_card_fields(card_attr, output_file)
for token in set_data.get("tokens"):
LOGGER.debug("Inserting token row for {}".format(token.get("name")))
token_attr = handle_token_row_insertion(token, set_code)
sql_dict_insert(token_attr, "tokens", sql_connection)
sql_dict_insert(token_attr, "tokens", output_file)
for language, translation in set_data.get("translations", {}).items():
LOGGER.debug("Inserting set_translation row for {}".format(language))
set_translation_attr = handle_set_translation_row_insertion(
language, translation, set_code
)
sql_dict_insert(
set_translation_attr, "set_translations", sql_connection
)
sql_dict_insert(set_translation_attr, "set_translations", output_file)
elif input_file.is_dir():
for setFile in input_file.glob("*.json"):
LOGGER.info("Loading {} into memory...".format(setFile.name))
@ -314,50 +341,53 @@ def parse_and_import_cards(
set_code = setFile.stem
LOGGER.info("Building set: {}".format(set_code))
set_insert_values = handle_set_row_insertion(set_data)
sql_dict_insert(set_insert_values, "sets", sql_connection)
sql_dict_insert(set_insert_values, "sets", output_file)
for card in set_data.get("cards"):
LOGGER.debug("Inserting card row for {}".format(card.get("name")))
card_attr: Dict[str, Any] = handle_card_row_insertion(card, set_code)
sql_insert_all_card_fields(card_attr, sql_connection)
sql_insert_all_card_fields(card_attr, output_file)
for token in set_data.get("tokens"):
LOGGER.debug("Inserting token row for {}".format(token.get("name")))
token_attr = handle_token_row_insertion(token, set_code)
sql_dict_insert(token_attr, "tokens", sql_connection)
sql_dict_insert(token_attr, "tokens", output_file)
for language, translation in set_data.get("translations", {}).items():
LOGGER.debug("Inserting set_translation row for {}".format(language))
set_translation_attr = handle_set_translation_row_insertion(
language, translation, set_code
)
sql_dict_insert(
set_translation_attr, "set_translations", sql_connection
)
sql_connection.commit()
sql_dict_insert(set_translation_attr, "set_translations", output_file)
if output_file["path"].suffix == ".sql":
output_file["handle"].write("COMMIT;")
else:
output_file["handle"].commit()
def sql_insert_all_card_fields(
card_attributes: Dict[str, Any], sql_connection: sqlite3.Connection
card_attributes: Dict[str, Any], output_file: Dict
) -> None:
"""
Given all of the card's data, insert the data into the
appropriate SQLite tables.
:param card_attributes: Tuple of data
:param sql_connection: DB Connection
:param output_file: Output info dictionary
"""
sql_dict_insert(card_attributes["cards"], "cards", sql_connection)
sql_dict_insert(card_attributes["cards"], "cards", output_file)
for foreign_val in card_attributes["foreignData"]:
sql_dict_insert(foreign_val, "foreignData", sql_connection)
sql_dict_insert(foreign_val, "foreignData", output_file)
for legal_val in card_attributes["legalities"]:
sql_dict_insert(legal_val, "legalities", sql_connection)
sql_dict_insert(legal_val, "legalities", output_file)
for rule_val in card_attributes["rulings"]:
sql_dict_insert(rule_val, "rulings", sql_connection)
sql_dict_insert(rule_val, "rulings", output_file)
for price_val in card_attributes["prices"]:
sql_dict_insert(price_val, "prices", sql_connection)
sql_dict_insert(price_val, "prices", output_file)
def handle_set_row_insertion(set_data: Dict[str, Any]) -> Dict[str, Any]:
@ -463,10 +493,10 @@ def handle_price_rows(
:return: List of dicts, ready for insertion
"""
prices = []
for type in card_data["prices"]:
for date, price in card_data["prices"][type].items():
for price_type in card_data["prices"]:
for date, price in card_data["prices"][price_type].items():
prices.append(
{"uuid": card_uuid, "type": type, "price": price, "date": date}
{"uuid": card_uuid, "type": price_type, "price": price, "date": date}
)
return prices
@ -553,7 +583,7 @@ def handle_card_row_insertion(
}
def modify_for_sql_insert(data: Any) -> Union[str, int, float]:
def modify_for_sql_insert(data: Any) -> Union[str, int, float, None]:
"""
Arrays and booleans can't be inserted, so we need to stringify
:param data: Data to modify
@ -578,17 +608,37 @@ def modify_for_sql_insert(data: Any) -> Union[str, int, float]:
return ""
def sql_dict_insert(
data: Dict[str, Any], table: str, sql_connection: sqlite3.Connection
) -> None:
def sql_dict_insert(data: Dict[str, Any], table: str, output_file: Dict) -> None:
"""
Insert a dictionary into a sqlite table
:param data: Dict to insert
:param table: Table to insert to
:param sql_connection: SQL connection
:param output_file: Output info dictionary
"""
cursor = sql_connection.cursor()
columns = ", ".join(data.keys())
placeholders = ":" + ", :".join(data.keys())
query = f"INSERT INTO {table} ({columns}) VALUES ({placeholders})"
cursor.execute(query, data)
if output_file["path"].suffix == ".sql":
for key in data.keys():
if isinstance(data[key], str):
data[key] = "'" + data[key].replace("'", "''").replace('"', '""') + "'"
if str(data[key]) == "False":
data[key] = 0
if str(data[key]) == "True":
data[key] = 1
if data[key] is None:
data[key] = "NULL"
query = (
"INSERT INTO "
+ table
+ " ("
+ ", ".join(data.keys())
+ ") VALUES ({"
+ "}, {".join(data.keys())
+ "});\n"
)
query = query.format(**data)
output_file["handle"].write(query)
else:
cursor = output_file["handle"].cursor()
columns = ", ".join(data.keys())
placeholders = ":" + ", :".join(data.keys())
query = f"INSERT INTO {table} ({columns}) VALUES ({placeholders})"
cursor.execute(query, data)