mirror of
https://github.com/ldericher/fftcgtool
synced 2025-01-15 15:02:59 +00:00
Mutable and Immutable CardDB variants based on click options
This commit is contained in:
parent
d585a3284d
commit
52651edca8
4 changed files with 145 additions and 88 deletions
|
@ -1,7 +1,7 @@
|
||||||
from .book import Book
|
from .book import Book
|
||||||
from .carddb import CardDB
|
from .carddb import CardDB, RWCardDB
|
||||||
from .language import Language
|
from .language import Language
|
||||||
from .opus import Opus
|
from .opus import Opus
|
||||||
from .ttsdeck import TTSDeck
|
from .ttsdeck import TTSDeck
|
||||||
|
|
||||||
__all__ = ["Book", "CardDB", "Language", "Opus", "TTSDeck"]
|
__all__ = ["Book", "CardDB", "RWCardDB", "Language", "Opus", "TTSDeck"]
|
||||||
|
|
128
fftcg/carddb.py
128
fftcg/carddb.py
|
@ -1,93 +1,121 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import pickle
|
import pickle
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from os import PathLike
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
from .card import Card
|
from .card import Card
|
||||||
from .cards import Cards
|
from .cards import Cards
|
||||||
from .code import Code
|
from .code import Code
|
||||||
from .language import API_LANGS
|
from .language import API_LANGS
|
||||||
from .utils import CARDDB_FILE_NAME
|
|
||||||
|
|
||||||
|
|
||||||
class CardDB:
|
class CardDB:
|
||||||
__instance: CardDB = None
|
_instance: CardDB = None
|
||||||
__cards: dict[Code, Card]
|
_cards: dict[Code, Card]
|
||||||
__face_to_url: dict[str, str]
|
_face_to_url: dict[str, str]
|
||||||
|
|
||||||
__DB_FILE_NAME = "cards.pickle"
|
_DB_FILE_NAME = "cards.pickle"
|
||||||
__MAPPING_FILE_NAME = "face_to_url.json"
|
_MAPPING_FILE_NAME = "face_to_url.json"
|
||||||
|
|
||||||
def __new__(cls) -> CardDB:
|
def __new__(cls, *more) -> CardDB:
|
||||||
if CardDB.__instance is None:
|
if CardDB._instance is None:
|
||||||
CardDB.__instance = object.__new__(cls)
|
CardDB._instance = object.__new__(CardDB)
|
||||||
CardDB.__instance.__cards = {}
|
|
||||||
CardDB.__instance.__face_to_url = {}
|
|
||||||
|
|
||||||
return CardDB.__instance
|
return CardDB._instance
|
||||||
|
|
||||||
|
def __init__(self, db_url: str = None):
|
||||||
|
if db_url is not None:
|
||||||
|
res = requests.get(db_url, stream=True)
|
||||||
|
if not res.ok:
|
||||||
|
raise ValueError("Invalid URL given to CardDB!")
|
||||||
|
|
||||||
|
self._load(io.BytesIO(res.content))
|
||||||
|
|
||||||
|
def _load(self, db: PathLike[str] | IO[bytes]):
|
||||||
|
try:
|
||||||
|
# unpickle db file
|
||||||
|
with zipfile.ZipFile(db, "r") as zip_file:
|
||||||
|
# cards db
|
||||||
|
with zip_file.open(CardDB._DB_FILE_NAME, "r") as file:
|
||||||
|
self._cards = pickle.load(file)
|
||||||
|
|
||||||
|
# face_to_url mapping
|
||||||
|
with zip_file.open(CardDB._MAPPING_FILE_NAME, "r") as file:
|
||||||
|
self._face_to_url = json.load(file)
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
self._cards = {}
|
||||||
|
self._face_to_url = {}
|
||||||
|
|
||||||
def __contains__(self, item: Code) -> bool:
|
def __contains__(self, item: Code) -> bool:
|
||||||
return item in self.__cards
|
return item in self._cards
|
||||||
|
|
||||||
def __getitem__(self, code: Code) -> Card:
|
def __getitem__(self, code: Code) -> Card:
|
||||||
return self.__cards[code]
|
return self._cards[code]
|
||||||
|
|
||||||
def get_face_url(self, face: str) -> str:
|
def get_face_url(self, face: str) -> str:
|
||||||
if face in self.__face_to_url:
|
if face in self._face_to_url:
|
||||||
return self.__face_to_url[face]
|
return self._face_to_url[face]
|
||||||
else:
|
else:
|
||||||
return face
|
return face
|
||||||
|
|
||||||
def __pickle(self) -> None:
|
def save(self) -> None:
|
||||||
with zipfile.ZipFile(CARDDB_FILE_NAME, "w", compression=zipfile.ZIP_LZMA) as zip_file:
|
return
|
||||||
|
|
||||||
|
def update(self, cards: Cards) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def upload_prompt(self) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class RWCardDB(CardDB):
|
||||||
|
__db_path: PathLike[str]
|
||||||
|
|
||||||
|
def __new__(cls, *more) -> RWCardDB:
|
||||||
|
if CardDB._instance is None:
|
||||||
|
CardDB._instance = object.__new__(RWCardDB)
|
||||||
|
|
||||||
|
return CardDB._instance
|
||||||
|
|
||||||
|
def __init__(self, db_path: PathLike[str] = None):
|
||||||
|
super().__init__(None)
|
||||||
|
|
||||||
|
if db_path is not None:
|
||||||
|
self.__db_path = db_path
|
||||||
|
self._load(self.__db_path)
|
||||||
|
|
||||||
|
def save(self) -> None:
|
||||||
|
with zipfile.ZipFile(self.__db_path, "w", compression=zipfile.ZIP_LZMA) as zip_file:
|
||||||
# cards db
|
# cards db
|
||||||
with zip_file.open(CardDB.__DB_FILE_NAME, "w") as file:
|
with zip_file.open(CardDB._DB_FILE_NAME, "w") as file:
|
||||||
pickle.dump(self.__cards, file)
|
pickle.dump(self._cards, file)
|
||||||
|
|
||||||
# face_to_url mapping
|
# face_to_url mapping
|
||||||
with zip_file.open(CardDB.__MAPPING_FILE_NAME, "w") as file:
|
with zip_file.open(CardDB._MAPPING_FILE_NAME, "w") as file:
|
||||||
file.write(json.dumps(self.__face_to_url, indent=2).encode("utf-8"))
|
file.write(json.dumps(self._face_to_url, indent=2).encode("utf-8"))
|
||||||
|
|
||||||
def __unpickle(self) -> None:
|
|
||||||
# unpickle db file
|
|
||||||
self.__cards.clear()
|
|
||||||
self.__face_to_url.clear()
|
|
||||||
try:
|
|
||||||
with zipfile.ZipFile(CARDDB_FILE_NAME, "r") as zip_file:
|
|
||||||
# cards db
|
|
||||||
with zip_file.open(CardDB.__DB_FILE_NAME, "r") as file:
|
|
||||||
self.__cards |= pickle.load(file)
|
|
||||||
|
|
||||||
# face_to_url mapping
|
|
||||||
with zip_file.open(CardDB.__MAPPING_FILE_NAME, "r") as file:
|
|
||||||
self.__face_to_url |= json.load(file)
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def load(self) -> None:
|
|
||||||
self.__unpickle()
|
|
||||||
|
|
||||||
def update(self, cards: Cards) -> None:
|
def update(self, cards: Cards) -> None:
|
||||||
for card in cards:
|
for card in cards:
|
||||||
self.__cards[card.code] = card
|
self._cards[card.code] = card
|
||||||
|
|
||||||
self.__pickle()
|
|
||||||
|
|
||||||
def upload_prompt(self) -> None:
|
def upload_prompt(self) -> None:
|
||||||
faces = list(set([
|
faces = list(set([
|
||||||
card[lang].face
|
card[lang].face
|
||||||
for card in self.__cards.values()
|
for card in self._cards.values()
|
||||||
for lang in API_LANGS
|
for lang in API_LANGS
|
||||||
if card[lang].face
|
if card[lang].face
|
||||||
]))
|
]))
|
||||||
faces.sort()
|
faces.sort()
|
||||||
|
|
||||||
for face in faces:
|
for face in faces:
|
||||||
if face not in self.__face_to_url:
|
if face not in self._face_to_url:
|
||||||
face_url = input(f"Upload '{face}' and paste URL: ")
|
face_url = input(f"Upload '{face}' and paste URL: ")
|
||||||
if face_url:
|
if face_url:
|
||||||
self.__face_to_url[face] = face_url
|
self._face_to_url[face] = face_url
|
||||||
|
|
||||||
self.__pickle()
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ GRID = Grid((10, 7)) # default in TTsim: 10 columns, 7 rows
|
||||||
RESOLUTION = Grid((429, 600)) # default in TTsim: 480x670 pixels per card
|
RESOLUTION = Grid((429, 600)) # default in TTsim: 480x670 pixels per card
|
||||||
DECKS_DIR_NAME = "decks" # name of decks directory
|
DECKS_DIR_NAME = "decks" # name of decks directory
|
||||||
IMAGES_DIR_NAME = "images" # name of images directory
|
IMAGES_DIR_NAME = "images" # name of images directory
|
||||||
CARDDB_FILE_NAME = "carddb.zip" # name of card db file
|
|
||||||
# card back URL (image by Aurik)
|
# card back URL (image by Aurik)
|
||||||
CARD_BACK_URL = "http://cloud-3.steamusercontent.com/ugc/948455238665576576/85063172B8C340602E8D6C783A457122F53F7843/"
|
CARD_BACK_URL = "http://cloud-3.steamusercontent.com/ugc/948455238665576576/85063172B8C340602E8D6C783A457122F53F7843/"
|
||||||
|
|
||||||
|
|
100
fftcgtool.py
100
fftcgtool.py
|
@ -8,13 +8,8 @@ import click
|
||||||
|
|
||||||
import fftcg
|
import fftcg
|
||||||
|
|
||||||
# constants
|
|
||||||
OUT_DIR_NAME = "out" # name of output directory
|
|
||||||
|
|
||||||
|
|
||||||
class LanguageParamType(click.ParamType):
|
class LanguageParamType(click.ParamType):
|
||||||
name = "lang"
|
|
||||||
|
|
||||||
def convert(self, value, param, ctx) -> fftcg.Language:
|
def convert(self, value, param, ctx) -> fftcg.Language:
|
||||||
if isinstance(value, fftcg.Language):
|
if isinstance(value, fftcg.Language):
|
||||||
return value
|
return value
|
||||||
|
@ -38,23 +33,53 @@ LANGUAGE = LanguageParamType()
|
||||||
type=LANGUAGE,
|
type=LANGUAGE,
|
||||||
default="en",
|
default="en",
|
||||||
help="language for imported objects",
|
help="language for imported objects",
|
||||||
|
metavar="LANG",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"-s", "--stdout",
|
"-z", "--zip",
|
||||||
is_flag=True,
|
type=click.File("wb"),
|
||||||
help="print the deck files in a zip archive to stdout, skip creating JSONs on disk",
|
help="wrap deck files into a zip archive, skip creating individual JSONs",
|
||||||
|
metavar="FILE",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-o", "--output",
|
||||||
|
type=click.Path(
|
||||||
|
allow_dash=False,
|
||||||
|
dir_okay=True,
|
||||||
|
file_okay=False,
|
||||||
|
),
|
||||||
|
help="use specified output directory instead of ./out",
|
||||||
|
default="out",
|
||||||
|
metavar="DIR",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-u", "--db-url",
|
||||||
|
type=str,
|
||||||
|
help="load immutable CardDB from URL instead of local, overrides -f",
|
||||||
|
metavar="URL",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-f", "--db-file",
|
||||||
|
type=click.Path(
|
||||||
|
allow_dash=False,
|
||||||
|
dir_okay=False,
|
||||||
|
file_okay=True,
|
||||||
|
),
|
||||||
|
default="carddb.zip",
|
||||||
|
help="use specified CardDB file instead of ./out/carddb.zip",
|
||||||
|
metavar="FILE",
|
||||||
)
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def main(ctx, verbose, language, stdout) -> None:
|
def main(ctx, **kwargs) -> None:
|
||||||
"""Imports FFTCG cards for TT-Sim."""
|
"""Imports FFTCG cards for TT-Sim."""
|
||||||
|
|
||||||
ctx.ensure_object(dict)
|
ctx.ensure_object(dict)
|
||||||
ctx.obj['LANG'] = language
|
ctx.obj["language"] = kwargs["language"]
|
||||||
|
|
||||||
# set up logging
|
# set up logging
|
||||||
if verbose == 0:
|
if kwargs["verbose"] == 0:
|
||||||
verbose = logging.WARN
|
verbose = logging.WARN
|
||||||
elif verbose == 1:
|
elif kwargs["verbose"] == 1:
|
||||||
verbose = logging.INFO
|
verbose = logging.INFO
|
||||||
else:
|
else:
|
||||||
verbose = logging.DEBUG
|
verbose = logging.DEBUG
|
||||||
|
@ -66,31 +91,39 @@ def main(ctx, verbose, language, stdout) -> None:
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.info("fftcgtool started.")
|
logger.info("fftcgtool started.")
|
||||||
logger.debug(f"args: {verbose = }, {language = }, {stdout = }")
|
logger.debug(f"{kwargs = }")
|
||||||
|
|
||||||
# output directory
|
# output directory
|
||||||
if not os.path.exists(OUT_DIR_NAME):
|
if not os.path.exists(kwargs["output"]):
|
||||||
os.mkdir(OUT_DIR_NAME)
|
os.mkdir(kwargs["output"])
|
||||||
|
|
||||||
os.chdir(OUT_DIR_NAME)
|
os.chdir(kwargs["output"])
|
||||||
|
|
||||||
# load the current carddb
|
# load the current carddb
|
||||||
carddb = fftcg.CardDB()
|
if kwargs["db_url"] is not None:
|
||||||
carddb.load()
|
try:
|
||||||
|
fftcg.CardDB(kwargs["db_url"])
|
||||||
|
|
||||||
|
except (ValueError, KeyError, zipfile.BadZipFile) as cause:
|
||||||
|
logger.critical(f"Couldn't initialize CardDB: {cause}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
else:
|
||||||
|
fftcg.RWCardDB(kwargs["db_file"])
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
@main.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
"-n", "--num_requests",
|
"-n", "--num-requests",
|
||||||
type=int,
|
type=int,
|
||||||
default=20,
|
default=20,
|
||||||
help="maximum number of concurrent requests",
|
help="maximum number of concurrent requests",
|
||||||
)
|
)
|
||||||
@click.argument(
|
@click.argument(
|
||||||
"opus_ids",
|
"opus-ids",
|
||||||
nargs=-1,
|
nargs=-1,
|
||||||
type=str,
|
type=str,
|
||||||
metavar="[OPUS_ID] ...",
|
metavar="[OPUS-ID] ...",
|
||||||
)
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def opuses(ctx, opus_ids, num_requests) -> list[fftcg.TTSDeck]:
|
def opuses(ctx, opus_ids, num_requests) -> list[fftcg.TTSDeck]:
|
||||||
|
@ -101,7 +134,7 @@ def opuses(ctx, opus_ids, num_requests) -> list[fftcg.TTSDeck]:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ctx.ensure_object(dict)
|
ctx.ensure_object(dict)
|
||||||
language = ctx.obj['LANG'] or fftcg.Language("")
|
language = ctx.obj["language"] or fftcg.Language("")
|
||||||
|
|
||||||
carddb = fftcg.CardDB()
|
carddb = fftcg.CardDB()
|
||||||
decks: list[fftcg.TTSDeck] = []
|
decks: list[fftcg.TTSDeck] = []
|
||||||
|
@ -115,6 +148,7 @@ def opuses(ctx, opus_ids, num_requests) -> list[fftcg.TTSDeck]:
|
||||||
decks.extend(opus.elemental_decks)
|
decks.extend(opus.elemental_decks)
|
||||||
|
|
||||||
carddb.upload_prompt()
|
carddb.upload_prompt()
|
||||||
|
carddb.save()
|
||||||
|
|
||||||
# create elemental decks for opus
|
# create elemental decks for opus
|
||||||
return decks
|
return decks
|
||||||
|
@ -122,10 +156,10 @@ def opuses(ctx, opus_ids, num_requests) -> list[fftcg.TTSDeck]:
|
||||||
|
|
||||||
@main.command()
|
@main.command()
|
||||||
@click.argument(
|
@click.argument(
|
||||||
"deck_ids",
|
"deck-ids",
|
||||||
nargs=-1,
|
nargs=-1,
|
||||||
type=str,
|
type=str,
|
||||||
metavar="[DECK_ID] ...",
|
metavar="[DECK-ID] ...",
|
||||||
)
|
)
|
||||||
def ffdecks(deck_ids) -> list[fftcg.TTSDeck]:
|
def ffdecks(deck_ids) -> list[fftcg.TTSDeck]:
|
||||||
"""
|
"""
|
||||||
|
@ -134,7 +168,6 @@ def ffdecks(deck_ids) -> list[fftcg.TTSDeck]:
|
||||||
DECK_ID: each of the Decks to import
|
DECK_ID: each of the Decks to import
|
||||||
"""
|
"""
|
||||||
|
|
||||||
print(f"{deck_ids = }")
|
|
||||||
decks: list[fftcg.TTSDeck] = []
|
decks: list[fftcg.TTSDeck] = []
|
||||||
for deck_id in deck_ids:
|
for deck_id in deck_ids:
|
||||||
# import a deck
|
# import a deck
|
||||||
|
@ -144,23 +177,20 @@ def ffdecks(deck_ids) -> list[fftcg.TTSDeck]:
|
||||||
|
|
||||||
|
|
||||||
@main.result_callback()
|
@main.result_callback()
|
||||||
def process_decks(decks: list[fftcg.TTSDeck], verbose, language, stdout):
|
def finalize(decks: list[fftcg.TTSDeck], **kwargs):
|
||||||
# arg needed because it's in this group
|
|
||||||
int(verbose)
|
|
||||||
|
|
||||||
# decide what to do with the decks
|
# decide what to do with the decks
|
||||||
if stdout:
|
if kwargs["zip"] is not None:
|
||||||
# print out a zip file
|
if decks:
|
||||||
with open(sys.stdout.fileno(), "wb", closefd=False, buffering=0) as raw_stdout:
|
# create zip file
|
||||||
with zipfile.ZipFile(raw_stdout, "w", compression=zipfile.ZIP_DEFLATED) as zip_file:
|
with zipfile.ZipFile(kwargs["zip"], "w", compression=zipfile.ZIP_DEFLATED) as zip_file:
|
||||||
# put the decks into that zip file
|
# put the decks into that zip file
|
||||||
for deck in decks:
|
for deck in decks:
|
||||||
zip_file.writestr(deck.file_name, deck.get_json(language))
|
zip_file.writestr(deck.file_name, deck.get_json(kwargs["language"]))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# save the decks to disk
|
# save the decks to disk
|
||||||
for deck in decks:
|
for deck in decks:
|
||||||
deck.save(language)
|
deck.save(kwargs["language"])
|
||||||
|
|
||||||
# bye
|
# bye
|
||||||
print("Done. Put the generated JSON files in your 'Saved Objects' Folder.")
|
print("Done. Put the generated JSON files in your 'Saved Objects' Folder.")
|
||||||
|
|
Loading…
Reference in a new issue