2020-07-06 19:54:42 -04:00
|
|
|
import argparse
|
2020-07-09 09:24:08 -04:00
|
|
|
import configparser
|
2021-07-03 14:23:48 -04:00
|
|
|
import copy
|
2020-07-07 10:13:56 -04:00
|
|
|
import datetime
|
2020-07-09 04:19:03 -04:00
|
|
|
import html
|
2021-07-03 14:23:48 -04:00
|
|
|
import io
|
2020-07-20 12:44:49 -04:00
|
|
|
import json
|
2020-07-06 19:54:42 -04:00
|
|
|
import logging
|
2020-07-22 12:04:13 -04:00
|
|
|
import pathlib
|
2021-07-03 14:23:48 -04:00
|
|
|
import pprint
|
2020-09-17 09:06:23 -04:00
|
|
|
import requests
|
2020-07-06 19:54:42 -04:00
|
|
|
import sys
|
2020-12-20 13:24:45 -05:00
|
|
|
import urllib
|
2020-07-06 19:54:42 -04:00
|
|
|
|
2020-07-22 12:04:13 -04:00
|
|
|
import appdirs
|
|
|
|
|
|
|
|
from . import export
|
|
|
|
from . import resources
|
|
|
|
from . import smashgg
|
|
|
|
from . import version
|
2020-07-07 10:13:56 -04:00
|
|
|
|
2021-03-03 03:12:35 -05:00
|
|
|
from .games import ssbu, pplus, melee
|
|
|
|
|
2020-07-20 17:54:32 -04:00
|
|
|
# =============================================================================
|
|
|
|
__version__ = version.__version__
|
2020-07-22 12:04:13 -04:00
|
|
|
__license__ = version.__license__
|
|
|
|
|
|
|
|
ROOTDIR = pathlib.Path(__file__).absolute().parent
|
2020-07-06 19:54:42 -04:00
|
|
|
|
2020-07-22 12:04:13 -04:00
|
|
|
APPDIRS = appdirs.AppDirs(version.NAME, version.ENTITY)
|
2020-07-20 18:34:07 -04:00
|
|
|
|
2020-12-20 13:24:45 -05:00
|
|
|
LOG_DUMMY = logging.getLogger("dummy")
|
|
|
|
LOG_DUMMY.addHandler(logging.NullHandler())
|
|
|
|
|
|
|
|
DEFAULT_DIR_TEMPLATES = ROOTDIR / "templates"
|
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
# =============================================================================
|
|
|
|
class StoreOptionKeyPair(argparse.Action):
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
option_strings,
|
|
|
|
dest,
|
|
|
|
nargs=1,
|
|
|
|
const=None,
|
|
|
|
default=None,
|
|
|
|
type=None,
|
|
|
|
choices=None,
|
|
|
|
required=False,
|
|
|
|
help=None,
|
|
|
|
metavar="KEY=VALUE",
|
|
|
|
):
|
|
|
|
|
|
|
|
if nargs == 0:
|
|
|
|
raise ValueError(
|
|
|
|
'nargs for append actions must be > 0; if arg '
|
|
|
|
'strings are not supplying the value to append, '
|
|
|
|
'the append const action may be more appropriate'
|
|
|
|
)
|
|
|
|
if const is not None and nargs != '?':
|
|
|
|
raise ValueError('nargs must be %r to supply const' % '?')
|
|
|
|
|
|
|
|
super(StoreOptionKeyPair, self).__init__(
|
|
|
|
option_strings=option_strings,
|
|
|
|
dest=dest,
|
|
|
|
nargs=nargs,
|
|
|
|
const=const,
|
|
|
|
default=default,
|
|
|
|
type=type,
|
|
|
|
choices=choices,
|
|
|
|
required=required,
|
|
|
|
help=help,
|
|
|
|
metavar=metavar,
|
|
|
|
)
|
|
|
|
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
|
|
try:
|
|
|
|
options = getattr(namespace, self.dest)
|
|
|
|
except AttributeError:
|
|
|
|
options = {}
|
|
|
|
|
|
|
|
for kv in values:
|
|
|
|
try:
|
|
|
|
k,v = kv.split("=")
|
|
|
|
except ValueError:
|
|
|
|
k = kv
|
|
|
|
v = True
|
|
|
|
|
|
|
|
options[k] = v
|
|
|
|
|
|
|
|
setattr(namespace, self.dest, options)
|
|
|
|
|
|
|
|
|
2020-12-20 13:24:45 -05:00
|
|
|
# =============================================================================
|
|
|
|
def get_templates_list(
|
|
|
|
dir_templates = DEFAULT_DIR_TEMPLATES,
|
|
|
|
):
|
|
|
|
|
|
|
|
templates_list = []
|
|
|
|
|
|
|
|
dir_templates_path = pathlib.Path(dir_templates)
|
|
|
|
|
|
|
|
for potential_template in dir_templates_path.iterdir():
|
|
|
|
|
|
|
|
if (potential_template / "template.svg.j2").is_file():
|
|
|
|
templates_list.append(potential_template.name)
|
|
|
|
|
|
|
|
return templates_list
|
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
# =============================================================================
|
|
|
|
def get_infos_from_file(
|
|
|
|
lkrz_file_path,
|
|
|
|
options = {},
|
|
|
|
outform = "dict",
|
|
|
|
log = LOG_DUMMY,
|
|
|
|
):
|
|
|
|
|
|
|
|
if not lkrz_file_path.exists():
|
|
|
|
raise IOError( "lkrz file '{}' does not exist" \
|
|
|
|
.format(str(lkrz_file_path)) )
|
|
|
|
|
|
|
|
lkrz = configparser.ConfigParser()
|
|
|
|
lkrz.read(str(lkrz_file_path))
|
|
|
|
|
|
|
|
log.info("Loading data from '{}'".format(str(lkrz_file_path)))
|
|
|
|
|
|
|
|
if s in lkrz:
|
|
|
|
section = lkrz[s]
|
|
|
|
if s == "Tournament":
|
|
|
|
tournament = smashgg.Tournament(
|
|
|
|
id = 0,
|
|
|
|
name = section["name"],
|
|
|
|
game = section["game"],
|
|
|
|
slug = section["slug"],
|
|
|
|
startAt = datetime.datetime.strptime(
|
|
|
|
section["date"],
|
|
|
|
"%Y-%m-%d %H:%M:%S",
|
|
|
|
),
|
|
|
|
numEntrants = int(section["numEntrants"]),
|
|
|
|
venueName = section["location"],
|
|
|
|
) \
|
|
|
|
.clean_name(
|
|
|
|
options.get("name_seo_delimiter", None)
|
|
|
|
)
|
|
|
|
|
|
|
|
elif s.startswith("player "):
|
|
|
|
chars = {}
|
|
|
|
for char in section["characters"].split(","):
|
|
|
|
c = char.strip()
|
|
|
|
charname = c.split("_")[0]
|
|
|
|
charskin = c.split("_")[1].split(" ")[0]
|
|
|
|
charscore = float(c.split("(")[1].split(")")[0])
|
|
|
|
|
|
|
|
chars[(charname,charskin)] = charscore
|
|
|
|
|
|
|
|
player = smashgg.Player(
|
|
|
|
id = 0,
|
|
|
|
prefix = section["team"],
|
|
|
|
gamerTag = section["tag"],
|
|
|
|
placement = section["placement"],
|
|
|
|
seeding = section["seeding"],
|
|
|
|
twitterHandle = section["twitter"],
|
|
|
|
chars = chars,
|
|
|
|
)
|
|
|
|
|
|
|
|
top_players[player.gamerTag] = player
|
|
|
|
|
|
|
|
# Re-sort top players by their placement
|
|
|
|
top_players = sorted(
|
|
|
|
top_players.values(),
|
|
|
|
key = lambda p: p.placement,
|
|
|
|
)
|
|
|
|
|
|
|
|
return format_infos(
|
|
|
|
outform,
|
|
|
|
tournament,
|
|
|
|
top_players,
|
|
|
|
)
|
|
|
|
|
2020-12-20 13:24:45 -05:00
|
|
|
# =============================================================================
|
|
|
|
def get_infos_from_url(
|
|
|
|
url,
|
|
|
|
token,
|
|
|
|
options = {},
|
|
|
|
outform = "dict",
|
|
|
|
top = 8,
|
|
|
|
proxy = None,
|
|
|
|
log = LOG_DUMMY,
|
|
|
|
):
|
|
|
|
|
|
|
|
url_parsed = urllib.parse.urlparse(url)
|
|
|
|
|
|
|
|
if url_parsed.netloc not in [ "smash.gg" ]:
|
|
|
|
raise ValueError("Unsupported domain name")
|
|
|
|
if outform not in [ "dict", "lkrz" ]:
|
|
|
|
raise ValueError("Unsupported outform")
|
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
tournament = None
|
|
|
|
event = None
|
|
|
|
|
2020-12-20 13:24:45 -05:00
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
if url_parsed.netloc == "smash.gg":
|
|
|
|
|
|
|
|
if (url_parsed.path.split("/")[1] != "tournament"):
|
2021-07-03 14:23:48 -04:00
|
|
|
log.error("Incomplete URL '{}'".format(url))
|
|
|
|
raise Exception("No tournament found in url {}".format(url))
|
2020-12-20 13:24:45 -05:00
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
try:
|
|
|
|
tournament = url_parsed.path.split("/")[2]
|
|
|
|
except:
|
|
|
|
log.error("Incomplete URL '{}'".format(url))
|
|
|
|
raise Exception("No tournament slug found in url {}".format(url))
|
2020-12-20 13:24:45 -05:00
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
try:
|
|
|
|
if (url_parsed.path.split("/")[3] == "event"):
|
|
|
|
event = url_parsed.path.split("/")[4]
|
|
|
|
except:
|
|
|
|
log.info("No event slug found in url")
|
|
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
return get_infos_from_id_or_slug(
|
|
|
|
id_or_slug = tournament,
|
|
|
|
event_slug = event,
|
|
|
|
token = token,
|
|
|
|
options = options,
|
|
|
|
outform = outform,
|
|
|
|
top = top,
|
|
|
|
proxy = proxy,
|
|
|
|
log = log,
|
|
|
|
)
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
def get_infos_from_id_or_slug(
|
|
|
|
id_or_slug,
|
|
|
|
event_slug = None,
|
|
|
|
token = "",
|
|
|
|
options = [],
|
|
|
|
outform = "dict",
|
|
|
|
top = 8,
|
|
|
|
proxy = None,
|
|
|
|
log = LOG_DUMMY,
|
|
|
|
):
|
|
|
|
# Get infos from smash.gg and write the config file
|
|
|
|
tournament, top_players = getTournamentTop(
|
|
|
|
id_or_slug = id_or_slug,
|
|
|
|
event_slug = event_slug,
|
|
|
|
import_options = options,
|
|
|
|
top = top,
|
|
|
|
token = token,
|
|
|
|
proxy = proxy,
|
|
|
|
log = log,
|
|
|
|
)
|
|
|
|
|
|
|
|
if tournament is None or top_players is None:
|
|
|
|
log.error("Could not load data from smash.gg")
|
|
|
|
raise Exception("Could not load data from smash.gg")
|
|
|
|
|
|
|
|
return format_infos(outform, tournament, top_players)
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
def format_infos(
|
|
|
|
outform,
|
|
|
|
tournament,
|
|
|
|
top_players,
|
|
|
|
):
|
2020-12-20 13:24:45 -05:00
|
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
if outform == "dict":
|
|
|
|
return {
|
|
|
|
"tournament": tournament,
|
|
|
|
"players": top_players,
|
|
|
|
}
|
|
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
if outform == "lkrz":
|
|
|
|
return "\n".join(
|
|
|
|
[ tournament.conf() ] \
|
|
|
|
+ list(map(
|
|
|
|
lambda p:p.conf(),
|
|
|
|
top_players,
|
|
|
|
))
|
|
|
|
)
|
|
|
|
|
2021-03-03 03:12:35 -05:00
|
|
|
# =============================================================================
|
|
|
|
def init_resources(
|
|
|
|
imgdir,
|
|
|
|
game,
|
|
|
|
source = None,
|
|
|
|
store_raw = False,
|
|
|
|
proxy = None,
|
|
|
|
log = LOG_DUMMY,
|
|
|
|
):
|
|
|
|
|
|
|
|
# Create imgdir
|
|
|
|
imgdir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
# Start resources download according to game
|
|
|
|
if game in ssbu.GAME.list_names():
|
2021-07-03 14:23:48 -04:00
|
|
|
game = ssbu
|
2021-03-03 03:12:35 -05:00
|
|
|
elif game in melee.GAME.list_names():
|
2021-07-03 14:23:48 -04:00
|
|
|
game = melee
|
2021-03-03 03:12:35 -05:00
|
|
|
elif game in pplus.GAME.list_names():
|
2021-07-03 14:23:48 -04:00
|
|
|
game = pplus
|
2021-03-03 03:12:35 -05:00
|
|
|
else:
|
|
|
|
log.error("Unknown game '{}'".format(game))
|
|
|
|
return 1
|
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
resources.download_res(
|
|
|
|
dstdir = imgdir,
|
|
|
|
game = game,
|
|
|
|
source = source,
|
|
|
|
store_raw = store_raw,
|
|
|
|
proxy = proxy,
|
|
|
|
log = log,
|
|
|
|
)
|
|
|
|
|
2021-03-03 03:12:35 -05:00
|
|
|
return 0
|
|
|
|
|
2020-12-20 13:24:45 -05:00
|
|
|
# =============================================================================
|
|
|
|
def generate_pic(
|
|
|
|
infos_or_lkrzfile = None,
|
|
|
|
template = None,
|
|
|
|
outform = "svg",
|
|
|
|
options = {},
|
|
|
|
dir_templates = DEFAULT_DIR_TEMPLATES,
|
|
|
|
dir_res = None,
|
|
|
|
dir_cache = None,
|
|
|
|
log = LOG_DUMMY,
|
|
|
|
):
|
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
if outform.startswith("."):
|
|
|
|
outform = outform[1:]
|
|
|
|
|
2020-12-20 13:24:45 -05:00
|
|
|
if outform not in ["svg", "png"]:
|
|
|
|
raise Exception("Unsupported outform")
|
|
|
|
|
|
|
|
if type(infos_or_lkrzfile) == str:
|
2021-07-03 14:23:48 -04:00
|
|
|
# load lkrz as dict infos
|
|
|
|
infos = get_infos_from_file(
|
|
|
|
lkrz_file_path = pathlib.Path(infos_or_lkrzfile),
|
|
|
|
options = options,
|
|
|
|
outform = "dict",
|
|
|
|
log = log,
|
|
|
|
)
|
2020-12-20 13:24:45 -05:00
|
|
|
else:
|
|
|
|
infos = infos_or_lkrzfile
|
|
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
# Build the context which will be passed to the template
|
|
|
|
context = {
|
|
|
|
"tournament": infos["tournament"].clean_name(
|
|
|
|
options.get(
|
|
|
|
"name_seo_delimiter",
|
|
|
|
None
|
|
|
|
)
|
|
|
|
),
|
|
|
|
"players" : sorted(
|
|
|
|
infos["players"],
|
|
|
|
key = lambda p: p.placement,
|
|
|
|
),
|
|
|
|
"dir_res_ssbu": dir_res,
|
|
|
|
"dir_template": str(dir_templates/template),
|
2021-07-03 14:23:48 -04:00
|
|
|
"options": options,
|
2020-12-20 13:24:45 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
pic = export.generate_pic(
|
|
|
|
dir_templates,
|
|
|
|
template,
|
|
|
|
context,
|
|
|
|
outform,
|
|
|
|
log = log,
|
|
|
|
cachedir = dir_cache,
|
2021-07-03 14:23:48 -04:00
|
|
|
options = options,
|
2020-12-20 13:24:45 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
if pic is None:
|
|
|
|
raise Exception("Failed to generate pic")
|
|
|
|
|
|
|
|
return pic
|
|
|
|
|
2020-07-06 19:54:42 -04:00
|
|
|
# =============================================================================
|
|
|
|
def main():
|
|
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
2020-07-20 18:35:56 -04:00
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
formatter_class = argparse.ArgumentDefaultsHelpFormatter,
|
|
|
|
)
|
2020-07-06 19:54:42 -04:00
|
|
|
|
|
|
|
subparsers = parser.add_subparsers(
|
|
|
|
dest = "command",
|
|
|
|
help = "commands",
|
|
|
|
)
|
|
|
|
|
2020-07-20 18:35:56 -04:00
|
|
|
parser.add_argument(
|
|
|
|
"--proxy", "-p",
|
|
|
|
default = None,
|
|
|
|
help = "the proxy to use",
|
|
|
|
)
|
2020-07-06 19:54:42 -04:00
|
|
|
|
2020-07-19 11:04:58 -04:00
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
init_parser = subparsers.add_parser(
|
|
|
|
"init",
|
2020-07-22 12:04:13 -04:00
|
|
|
formatter_class = argparse.ArgumentDefaultsHelpFormatter,
|
2020-07-19 11:04:58 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
init_parser.add_argument(
|
|
|
|
"game",
|
|
|
|
default = "ssbu",
|
|
|
|
help = "The game you want to initialize the resources for",
|
|
|
|
)
|
|
|
|
|
2021-03-03 03:12:35 -05:00
|
|
|
init_parser.add_argument(
|
|
|
|
"--source", "-s",
|
|
|
|
default = None,
|
|
|
|
choices = ["spriters", "smashlyon"],
|
|
|
|
help = "From where should the resources images be downloaded",
|
|
|
|
)
|
|
|
|
|
2020-07-19 11:04:58 -04:00
|
|
|
init_parser.add_argument(
|
|
|
|
"--imgdir", "-ID",
|
2020-07-22 12:04:13 -04:00
|
|
|
type = pathlib.Path,
|
|
|
|
default = pathlib.Path(APPDIRS.user_data_dir) / "res",
|
2020-07-19 11:04:58 -04:00
|
|
|
help = "The directory we should download the resources to",
|
|
|
|
)
|
|
|
|
|
2021-03-03 03:12:35 -05:00
|
|
|
init_parser.add_argument(
|
|
|
|
"--raw", "-r",
|
|
|
|
action = "store_true",
|
|
|
|
help = "Download the raw zipfiles instead of extracting them",
|
|
|
|
)
|
|
|
|
|
2020-07-06 19:54:42 -04:00
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
top8_parser = subparsers.add_parser(
|
|
|
|
"top8",
|
2020-07-22 12:04:13 -04:00
|
|
|
formatter_class = argparse.ArgumentDefaultsHelpFormatter,
|
2020-07-06 19:54:42 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
top8_parser.add_argument(
|
|
|
|
"tournament",
|
|
|
|
default = None,
|
2021-03-03 03:12:35 -05:00
|
|
|
help = "The tournament url, slug or id",
|
2020-07-06 19:54:42 -04:00
|
|
|
)
|
2020-07-22 12:04:13 -04:00
|
|
|
|
|
|
|
top8_parser.add_argument(
|
|
|
|
"--token", "-t",
|
|
|
|
default = None,
|
2021-03-03 03:12:35 -05:00
|
|
|
help = "the authentication token to use ; needed if you're " \
|
|
|
|
"generating the top8 from a smash.gg url",
|
|
|
|
)
|
|
|
|
|
|
|
|
top8_parser.add_argument(
|
|
|
|
"--playerskinsdb", "-P",
|
|
|
|
type = (lambda s: s if s.startswith("http") else pathlib.Path(s)),
|
|
|
|
default = ROOTDIR / "data" / "playerskinsdb.json",
|
|
|
|
help = "A JSON file path or url matching player tags, characters,"\
|
|
|
|
" sponsors, and preferred skins",
|
2020-07-22 12:04:13 -04:00
|
|
|
)
|
|
|
|
|
2020-07-09 04:19:03 -04:00
|
|
|
top8_parser.add_argument(
|
|
|
|
"--imgdir", "-ID",
|
2020-07-22 12:04:13 -04:00
|
|
|
type = pathlib.Path,
|
|
|
|
default = pathlib.Path(APPDIRS.user_data_dir) / "res",
|
2020-07-20 18:34:07 -04:00
|
|
|
help = "The directories containing images, be careful whether " \
|
|
|
|
"you specify an absolute path or a relative one.",
|
2020-07-07 10:13:56 -04:00
|
|
|
)
|
2020-08-28 06:49:55 -04:00
|
|
|
top8_parser.add_argument(
|
|
|
|
"--cachedir", "-CD",
|
|
|
|
type = pathlib.Path,
|
|
|
|
default = pathlib.Path(APPDIRS.user_cache_dir),
|
|
|
|
help = "A directory to use for temporary files",
|
|
|
|
)
|
2020-07-07 10:13:56 -04:00
|
|
|
top8_parser.add_argument(
|
|
|
|
"--templatesdir", "-TD",
|
2020-07-22 12:04:13 -04:00
|
|
|
type = pathlib.Path,
|
2020-12-20 13:24:45 -05:00
|
|
|
default = DEFAULT_DIR_TEMPLATES,
|
2020-07-07 10:13:56 -04:00
|
|
|
help = "The local result templates directory",
|
|
|
|
)
|
2020-09-02 03:07:36 -04:00
|
|
|
|
2020-07-06 19:54:42 -04:00
|
|
|
top8_parser.add_argument(
|
|
|
|
"--template", "-T",
|
2020-07-20 18:35:56 -04:00
|
|
|
default = "rebootlyon2020",
|
2020-07-06 19:54:42 -04:00
|
|
|
help = "The local result template to use",
|
|
|
|
)
|
2020-07-25 08:51:10 -04:00
|
|
|
top8_parser.add_argument(
|
2021-03-03 03:12:35 -05:00
|
|
|
"--template-options", "-TO",
|
2021-07-03 14:23:48 -04:00
|
|
|
nargs="+",
|
|
|
|
action = StoreOptionKeyPair,
|
|
|
|
default = {},
|
2021-03-03 03:12:35 -05:00
|
|
|
help = "Template-specific options (like 'covid' or 'animated')",
|
2020-07-25 08:51:10 -04:00
|
|
|
)
|
2021-01-06 03:33:36 -05:00
|
|
|
top8_parser.add_argument(
|
2021-03-03 03:12:35 -05:00
|
|
|
"--export-options", "-EO",
|
2021-07-03 14:23:48 -04:00
|
|
|
nargs="+",
|
|
|
|
action = StoreOptionKeyPair,
|
|
|
|
default = {},
|
2021-03-03 03:12:35 -05:00
|
|
|
help = "Export options (like 'svg_embed_png')",
|
2021-01-06 03:33:36 -05:00
|
|
|
)
|
2020-09-02 03:07:36 -04:00
|
|
|
|
2020-07-06 19:54:42 -04:00
|
|
|
top8_parser.add_argument(
|
2021-03-03 03:12:35 -05:00
|
|
|
"--import-options", "-IO",
|
2021-07-03 14:23:48 -04:00
|
|
|
nargs="+",
|
|
|
|
action = StoreOptionKeyPair,
|
|
|
|
default = {},
|
2021-03-03 03:12:35 -05:00
|
|
|
help = "Import options (like 'use_smashgg_prefixes')",
|
2020-07-19 16:26:34 -04:00
|
|
|
)
|
2021-03-03 03:12:35 -05:00
|
|
|
|
2020-07-07 10:13:56 -04:00
|
|
|
top8_parser.add_argument(
|
|
|
|
"--outfile", "-o",
|
2020-07-22 12:04:13 -04:00
|
|
|
type = pathlib.Path,
|
2020-07-07 10:13:56 -04:00
|
|
|
default = None,
|
2020-07-22 12:04:13 -04:00
|
|
|
help = "The SVG or PNG local result file to output to ; if it's " \
|
2021-03-03 03:12:35 -05:00
|
|
|
"not specified, it will default to SVG and use the " \
|
|
|
|
"tournament slug as name ; if you're generating a " \
|
|
|
|
"localresult from a url, a LKRZ file with the same name " \
|
|
|
|
"will also be generated along the image file (unless you " \
|
|
|
|
"use the --no-lkrz flag).",
|
2020-07-19 16:26:34 -04:00
|
|
|
)
|
2020-09-02 03:07:36 -04:00
|
|
|
|
2021-03-03 03:12:35 -05:00
|
|
|
parser.add_argument( "--no-lkrz", "-nl",
|
|
|
|
default = False,
|
|
|
|
action = "store_true",
|
|
|
|
help = "Do not output a LKRZ file" )
|
2020-07-07 10:13:56 -04:00
|
|
|
|
2020-07-06 19:54:42 -04:00
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
parser.add_argument( "--verbose", "-v",
|
|
|
|
default = 0,
|
|
|
|
action = "count",
|
|
|
|
help = "increase verbosity" )
|
|
|
|
|
|
|
|
parser.add_argument( "--version", "-V",
|
|
|
|
default = False,
|
|
|
|
action = "store_true",
|
|
|
|
help = "show version number" )
|
|
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
# Set log level
|
|
|
|
# -------------------------------------------------------------------------
|
2020-07-07 10:13:56 -04:00
|
|
|
log = logging.getLogger(version.NAME)
|
2020-07-06 19:54:42 -04:00
|
|
|
log.setLevel(logging.DEBUG)
|
|
|
|
|
|
|
|
log_handler_console = logging.StreamHandler()
|
|
|
|
log_handler_console.setLevel(logging.WARNING)
|
|
|
|
|
|
|
|
if(args.verbose >= 2):
|
|
|
|
log_handler_console.setLevel(logging.DEBUG)
|
|
|
|
elif(args.verbose >=1):
|
|
|
|
log_handler_console.setLevel(logging.INFO)
|
|
|
|
else:
|
|
|
|
log_handler_console.setLevel(logging.WARNING)
|
|
|
|
|
|
|
|
log_formatter_console = logging.Formatter("%(name)s:%(levelname)s: %(message)s")
|
|
|
|
|
|
|
|
log_handler_console.setFormatter(log_formatter_console)
|
|
|
|
|
|
|
|
log.addHandler(log_handler_console)
|
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
# Print all arguments in debug
|
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
log.debug( "Command arguments:\n{}".format( pprint.pformat(vars(args))) )
|
|
|
|
|
2020-07-06 19:54:42 -04:00
|
|
|
# Print version if required
|
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
if args.version:
|
2020-07-07 10:13:56 -04:00
|
|
|
print(version.VERSION_NAME)
|
2020-07-09 08:31:22 -04:00
|
|
|
return 0
|
2020-07-06 19:54:42 -04:00
|
|
|
|
2021-03-03 03:12:35 -05:00
|
|
|
# Check if command is recognized
|
2020-07-07 10:13:56 -04:00
|
|
|
# -------------------------------------------------------------------------
|
2020-07-19 11:04:58 -04:00
|
|
|
if args.command not in [ "init", "top8" ]:
|
2020-07-07 10:13:56 -04:00
|
|
|
parser.print_help()
|
2020-07-09 08:31:22 -04:00
|
|
|
return 1
|
2020-07-07 10:13:56 -04:00
|
|
|
|
2021-03-03 03:12:35 -05:00
|
|
|
# -- init
|
2020-07-07 10:13:56 -04:00
|
|
|
# -------------------------------------------------------------------------
|
2020-07-19 11:04:58 -04:00
|
|
|
if args.command == "init":
|
2021-03-03 03:12:35 -05:00
|
|
|
|
|
|
|
rv = init_resources(
|
|
|
|
imgdir = args.imgdir,
|
|
|
|
game = args.game,
|
|
|
|
source = args.source,
|
|
|
|
store_raw = args.raw,
|
2020-07-22 12:04:13 -04:00
|
|
|
proxy = args.proxy,
|
2020-07-19 11:04:58 -04:00
|
|
|
log = log,
|
|
|
|
)
|
2021-03-03 03:12:35 -05:00
|
|
|
|
|
|
|
return rv
|
|
|
|
|
|
|
|
# -- top8
|
2020-07-19 11:04:58 -04:00
|
|
|
# -------------------------------------------------------------------------
|
2020-07-07 10:13:56 -04:00
|
|
|
if args.command == "top8":
|
|
|
|
|
2020-07-20 12:44:49 -04:00
|
|
|
# Initialize PLAYERSKINS db
|
|
|
|
log.debug("loading playerskins db from '{}'" \
|
|
|
|
.format(args.playerskinsdb))
|
2020-09-17 09:06:23 -04:00
|
|
|
try:
|
2020-12-20 13:24:45 -05:00
|
|
|
PLAYERSKINS = requests.get(args.playerskinsdb).json()
|
|
|
|
smashgg.GET_PLAYERDATA = (lambda tag: PLAYERSKINS[tag.lower()])
|
2020-09-17 09:06:23 -04:00
|
|
|
except:
|
|
|
|
with args.playerskinsdb.open("r", encoding="utf8") as f:
|
2020-12-20 13:24:45 -05:00
|
|
|
PLAYERSKINS = json.load(f)
|
|
|
|
smashgg.GET_PLAYERDATA = (lambda tag: PLAYERSKINS[tag.lower()])
|
2020-07-20 12:44:49 -04:00
|
|
|
|
|
|
|
#
|
2020-07-09 09:24:08 -04:00
|
|
|
tournament = None
|
|
|
|
top_players = {}
|
2021-07-03 14:23:48 -04:00
|
|
|
lkrz_file = None
|
2020-07-09 09:24:08 -04:00
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
all_options = {
|
|
|
|
**args.import_options,
|
|
|
|
**args.template_options,
|
|
|
|
**args.export_options,
|
|
|
|
}
|
2020-07-07 10:13:56 -04:00
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
# Determine the nature of the 'tournament' argument :
|
|
|
|
# - url
|
|
|
|
# - id or slug
|
|
|
|
# - lkrz file
|
|
|
|
# url
|
|
|
|
if ( args.tournament.startswith("http://")
|
|
|
|
or args.tournament.startswith("https://") ):
|
2021-03-03 03:12:35 -05:00
|
|
|
infos = get_infos_from_url(
|
|
|
|
url = args.tournament,
|
2020-07-07 10:13:56 -04:00
|
|
|
token = args.token,
|
2021-03-03 03:12:35 -05:00
|
|
|
options = args.import_options,
|
|
|
|
outform = "dict",
|
|
|
|
top = 8,
|
2020-07-07 10:13:56 -04:00
|
|
|
proxy = args.proxy,
|
|
|
|
log = log,
|
|
|
|
)
|
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
# lkrz file
|
|
|
|
elif pathlib.Path(args.tournament).exists():
|
|
|
|
infos = get_infos_from_file(
|
|
|
|
lkrz_file_path = pathlib.Path(args.tournament),
|
|
|
|
options = args.import_options,
|
|
|
|
outform = "dict",
|
|
|
|
log = log,
|
|
|
|
)
|
2020-07-07 10:13:56 -04:00
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
# id or slug
|
|
|
|
else:
|
|
|
|
infos = get_infos_from_id_or_slug(
|
|
|
|
id_or_slug = args.tournament,
|
|
|
|
token = args.token,
|
|
|
|
options = args.import_options,
|
|
|
|
outform = "dict",
|
|
|
|
top = 8,
|
|
|
|
proxy = args.proxy,
|
|
|
|
log = log,
|
2020-07-07 10:13:56 -04:00
|
|
|
)
|
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
tournament = infos["tournament"]
|
|
|
|
top_players = infos["players"]
|
2020-07-07 10:13:56 -04:00
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
if tournament is None or top_players is None:
|
|
|
|
log.error("Could not load data")
|
|
|
|
return 1
|
2020-07-07 10:13:56 -04:00
|
|
|
|
2020-07-09 09:24:08 -04:00
|
|
|
# Default outfile is 'tournament-slug.svg'
|
|
|
|
if args.outfile is None:
|
2020-07-22 12:04:13 -04:00
|
|
|
args.outfile = pathlib.Path(
|
|
|
|
"{}.svg".format(tournament.slug),
|
|
|
|
)
|
2020-07-09 09:24:08 -04:00
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
# Save a lkrz file
|
|
|
|
if not args.no_lkrz:
|
|
|
|
lkrz_data = format_infos("lkrz", tournament, top_players)
|
|
|
|
lkrz_file = args.outfile.with_suffix(".lkrz")
|
|
|
|
|
|
|
|
with lkrz_file.open("w", encoding="utf8") as f:
|
|
|
|
f.write(lkrz_data)
|
|
|
|
|
|
|
|
# If the outfile we were asked for was a .lkrz, we're done
|
|
|
|
if args.outfile.suffix == ".lkrz":
|
|
|
|
return 0
|
|
|
|
|
|
|
|
# Otherwise, let's generate the picture file
|
|
|
|
# First build the context which will be passed to the template
|
2020-07-22 14:28:42 -04:00
|
|
|
try:
|
2021-07-03 14:23:48 -04:00
|
|
|
dir_res = (args.imgdir / tournament.game.name).as_uri() # not absolute => error
|
2020-07-22 14:28:42 -04:00
|
|
|
except ValueError:
|
2021-07-03 14:23:48 -04:00
|
|
|
dir_res = (args.imgdir / tournament.game.name).as_posix()
|
|
|
|
|
|
|
|
pic = generate_pic(
|
|
|
|
infos_or_lkrzfile = infos,
|
|
|
|
template = args.template,
|
|
|
|
outform = args.outfile.suffix,
|
|
|
|
options = all_options,
|
|
|
|
dir_templates = args.templatesdir,
|
|
|
|
dir_res = dir_res,
|
|
|
|
dir_cache = args.cachedir,
|
2020-07-22 12:04:13 -04:00
|
|
|
log = log,
|
2020-07-09 08:31:22 -04:00
|
|
|
)
|
2020-07-07 10:13:56 -04:00
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
if pic is None:
|
2020-07-09 08:31:22 -04:00
|
|
|
return 1
|
2020-07-07 10:13:56 -04:00
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
log.info("Saving picture as '{}'".format(args.outfile))
|
|
|
|
|
|
|
|
if type(pic) == io.StringIO:
|
|
|
|
openmode = "w"
|
|
|
|
else:
|
|
|
|
openmode = "wb"
|
|
|
|
with args.outfile.open(openmode) as f:
|
|
|
|
f.write(pic.read())
|
2020-07-07 10:13:56 -04:00
|
|
|
|
2020-07-09 08:31:22 -04:00
|
|
|
return 0
|
2020-07-07 10:13:56 -04:00
|
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
|
|
def getTournamentTop(
|
|
|
|
id_or_slug,
|
2021-07-03 14:23:48 -04:00
|
|
|
event_slug = None,
|
|
|
|
import_options = [],
|
2021-01-05 08:14:25 -05:00
|
|
|
top = 8,
|
2020-07-07 10:13:56 -04:00
|
|
|
token = "",
|
|
|
|
proxy = None,
|
2021-07-03 14:23:48 -04:00
|
|
|
log=LOG_DUMMY,
|
|
|
|
):
|
2020-07-07 10:13:56 -04:00
|
|
|
"""Returns a tuple : the smashgg.Tournament object and a list of the top
|
|
|
|
smashgg.Player in that tournament."""
|
|
|
|
|
2020-12-03 03:36:17 -05:00
|
|
|
# TODO if url matches challonge
|
|
|
|
#
|
|
|
|
#data = challonge.get_participants(
|
|
|
|
# api_key = token,
|
|
|
|
# tournament = id_or_slug,
|
|
|
|
# )
|
|
|
|
#
|
|
|
|
#top_array = []*top
|
|
|
|
#for p in data:
|
|
|
|
# top_array[p["participant"]["final_rank"]] = ...
|
2020-07-07 10:13:56 -04:00
|
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
# Select the right event (the one with the most entrants or the most sets)
|
2020-12-20 13:24:45 -05:00
|
|
|
def selectBiggestEvent(data, log=LOG_DUMMY):
|
2020-07-07 10:13:56 -04:00
|
|
|
|
|
|
|
try:
|
|
|
|
event = data["events"][0]
|
|
|
|
except:
|
|
|
|
log.error("No event found in data")
|
|
|
|
log.debug(data)
|
|
|
|
return None
|
|
|
|
|
|
|
|
try:
|
|
|
|
numEntrants = event["numEntrants"]
|
|
|
|
except KeyError:
|
|
|
|
numEntrants = event["standings"]["pageInfo"]["total"]
|
|
|
|
|
|
|
|
for e in data["events"]:
|
|
|
|
try:
|
|
|
|
ne = e["numEntrants"]
|
|
|
|
except KeyError:
|
|
|
|
ne = e["standings"]["pageInfo"]["total"]
|
|
|
|
|
|
|
|
if ne > numEntrants:
|
|
|
|
event = e
|
|
|
|
numEntrants = ne
|
|
|
|
|
|
|
|
log.info("Selected Event '{}' with {} entrants" \
|
|
|
|
.format(
|
|
|
|
event["name"],
|
|
|
|
numEntrants,
|
|
|
|
))
|
|
|
|
|
|
|
|
return event
|
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
# Select the specified event
|
|
|
|
def selectEventBySlug(data, slug, log=LOG_DUMMY):
|
|
|
|
|
|
|
|
try:
|
|
|
|
event = data["events"][0]
|
|
|
|
except:
|
|
|
|
log.error("No event found in data")
|
|
|
|
log.debug(data)
|
|
|
|
return None
|
|
|
|
|
|
|
|
for e in data["events"]:
|
|
|
|
try:
|
|
|
|
slug_full = e["slug"]
|
|
|
|
except KeyError:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if ( slug == slug_full
|
|
|
|
or slug == slug_full.split("/")[-1] ):
|
|
|
|
log.info("Selected Event '{}' by slug '{}'" \
|
|
|
|
.format(
|
|
|
|
e["name"],
|
|
|
|
slug,
|
|
|
|
))
|
|
|
|
return e
|
|
|
|
|
|
|
|
|
|
|
|
log.error("No Event matching slug '{}' found".format(slug))
|
|
|
|
return None
|
|
|
|
|
2020-07-07 10:13:56 -04:00
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
data = None
|
|
|
|
|
|
|
|
try:
|
|
|
|
data = smashgg.run_query(
|
|
|
|
query_name = "getTournamentTopById",
|
|
|
|
variables = {
|
|
|
|
"id" : int(id_or_slug), # If this fails, it's a slug
|
|
|
|
"top": top,
|
|
|
|
},
|
2020-07-22 12:04:13 -04:00
|
|
|
query_dir = ROOTDIR / "queries",
|
2020-07-07 10:13:56 -04:00
|
|
|
token = token,
|
|
|
|
proxy = proxy,
|
|
|
|
log = log,
|
|
|
|
)
|
|
|
|
|
|
|
|
except ValueError:
|
|
|
|
data = smashgg.run_query(
|
|
|
|
query_name = "getTournamentTopBySlug",
|
|
|
|
variables = {
|
|
|
|
"slug" : id_or_slug,
|
|
|
|
"top": top,
|
|
|
|
},
|
2020-07-22 12:04:13 -04:00
|
|
|
query_dir = ROOTDIR / "queries",
|
2020-07-07 10:13:56 -04:00
|
|
|
token = token,
|
|
|
|
proxy = proxy,
|
|
|
|
log = log,
|
|
|
|
)
|
|
|
|
|
|
|
|
try:
|
|
|
|
tournament_data = data["tournament"]
|
|
|
|
except:
|
|
|
|
log.error("Failed to load Tournaments")
|
|
|
|
return None,None
|
|
|
|
|
|
|
|
if tournament_data is None:
|
|
|
|
log.error("Failed to load Tournament")
|
|
|
|
return None,None
|
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
event = None
|
|
|
|
if event_slug is None:
|
|
|
|
event = selectBiggestEvent(tournament_data, log)
|
|
|
|
else:
|
|
|
|
event = selectEventBySlug(tournament_data, event_slug, log)
|
2020-07-07 10:13:56 -04:00
|
|
|
|
|
|
|
if event is None :
|
|
|
|
return None,None
|
|
|
|
|
|
|
|
# Get the tournament
|
|
|
|
tournament = smashgg.Tournament(
|
|
|
|
id = tournament_data["id"],
|
|
|
|
slug = tournament_data["slug"],
|
2021-07-03 14:23:48 -04:00
|
|
|
game = event["videogame"],
|
2020-07-07 10:13:56 -04:00
|
|
|
name = tournament_data["name"],
|
|
|
|
startAt = \
|
|
|
|
datetime.datetime. \
|
|
|
|
fromtimestamp(tournament_data["startAt"]),
|
|
|
|
numEntrants = event["standings"]["pageInfo"]["total"],
|
|
|
|
venueAddress = tournament_data["venueAddress"],
|
|
|
|
venueName = tournament_data["venueName"],
|
|
|
|
city = tournament_data["city"],
|
|
|
|
countryCode = tournament_data["countryCode"],
|
|
|
|
hashtag = tournament_data["hashtag"],
|
2021-07-03 14:23:48 -04:00
|
|
|
) \
|
|
|
|
.clean_name(
|
|
|
|
import_options.get("name_seo_delimiter", None)
|
|
|
|
)
|
2020-07-07 10:13:56 -04:00
|
|
|
|
|
|
|
|
|
|
|
# Get the top players
|
|
|
|
top_players = {}
|
|
|
|
|
|
|
|
standings = event["standings"]["nodes"]
|
|
|
|
|
|
|
|
for standing in standings :
|
|
|
|
|
|
|
|
seeding = None
|
|
|
|
for seed in standing["entrant"]["seeds"]:
|
|
|
|
# Take the seeding from the phase with *all* Event entrants
|
|
|
|
if seed["phase"]["numSeeds"] == tournament.numEntrants:
|
|
|
|
seeding = seed["groupSeedNum"]
|
|
|
|
|
|
|
|
participant_data = standing["entrant"]["participants"][0]
|
|
|
|
|
|
|
|
try:
|
|
|
|
twitterHandle = participant_data \
|
|
|
|
["player"] \
|
|
|
|
["user"] \
|
|
|
|
["authorizations"] \
|
|
|
|
[0] \
|
|
|
|
["externalUsername"]
|
2021-07-03 14:23:48 -04:00
|
|
|
except:
|
2020-07-07 10:13:56 -04:00
|
|
|
twitterHandle = None
|
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
if "use_smashgg_prefixes" in import_options:
|
2020-09-02 03:07:36 -04:00
|
|
|
prefix = participant_data["prefix"]
|
|
|
|
else:
|
|
|
|
prefix = ""
|
2020-07-07 10:13:56 -04:00
|
|
|
|
|
|
|
player = smashgg.Player(
|
|
|
|
id = standing["entrant"]["id"],
|
2020-09-02 03:07:36 -04:00
|
|
|
prefix = prefix,
|
2020-07-07 10:13:56 -04:00
|
|
|
gamerTag = participant_data["gamerTag"],
|
|
|
|
placement = standing["placement"],
|
|
|
|
seeding = seeding,
|
|
|
|
twitterHandle = twitterHandle,
|
|
|
|
)
|
|
|
|
|
|
|
|
top_players[player.id] = player
|
|
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
# Now, we need to find which characters those top players chose
|
|
|
|
data = None
|
|
|
|
|
|
|
|
data = smashgg.run_query(
|
|
|
|
query_name = "getCharsByTournamentIdAndEntrantIds",
|
|
|
|
variables = {
|
|
|
|
"tournamentId" : int(tournament.id),
|
|
|
|
"entrantIds": [ id for id in top_players.keys() ],
|
|
|
|
},
|
2020-07-22 12:04:13 -04:00
|
|
|
query_dir = ROOTDIR / "queries",
|
2020-07-07 10:13:56 -04:00
|
|
|
token = token,
|
|
|
|
proxy = proxy,
|
|
|
|
log = log,
|
|
|
|
)
|
|
|
|
|
|
|
|
try:
|
|
|
|
tournament_data = data["tournament"]
|
|
|
|
except:
|
|
|
|
log.error("Failed to load Tournament")
|
|
|
|
return None,None
|
|
|
|
|
|
|
|
if tournament_data is None:
|
|
|
|
log.error("Failed to load Tournament")
|
|
|
|
return None,None
|
|
|
|
|
2021-07-03 14:23:48 -04:00
|
|
|
event = None
|
|
|
|
if event_slug is None:
|
|
|
|
event = selectBiggestEvent(tournament_data, log)
|
|
|
|
else:
|
|
|
|
event = selectEventBySlug(tournament_data, event_slug, log)
|
2020-07-07 10:13:56 -04:00
|
|
|
|
|
|
|
if event is None :
|
|
|
|
return None,None
|
|
|
|
|
|
|
|
# TODO check that sets number is < to hardcoded 100 max value (cf query)
|
|
|
|
sets = event["sets"]["nodes"]
|
|
|
|
|
|
|
|
for s in sets:
|
|
|
|
try:
|
|
|
|
for g in s["games"]:
|
|
|
|
|
|
|
|
winnerId = g["winnerId"]
|
|
|
|
|
|
|
|
for slct in g["selections"]:
|
|
|
|
|
|
|
|
if slct["selectionType"] == "CHARACTER":
|
|
|
|
eid = slct["entrant"]["id"]
|
|
|
|
try:
|
|
|
|
top_players[eid].add_character_selection(
|
2021-07-03 14:23:48 -04:00
|
|
|
game = tournament.game,
|
2020-07-07 10:13:56 -04:00
|
|
|
character = slct["selectionValue"],
|
|
|
|
win = (winnerId == eid),
|
|
|
|
)
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
except TypeError:
|
|
|
|
# If some games or selections are null, this can happen
|
|
|
|
continue
|
|
|
|
|
2020-12-20 13:24:45 -05:00
|
|
|
# Sort top_players by rank instead of id:
|
|
|
|
top_players_sorted = sorted(
|
|
|
|
top_players.values(),
|
|
|
|
key = lambda p: p.placement,
|
|
|
|
)
|
2020-07-07 10:13:56 -04:00
|
|
|
|
|
|
|
# Return the data
|
2020-12-20 13:24:45 -05:00
|
|
|
return tournament, top_players_sorted
|
2020-07-07 10:13:56 -04:00
|
|
|
|
2020-07-06 19:54:42 -04:00
|
|
|
# =============================================================================
|
|
|
|
if __name__ == '__main__':
|
2020-07-09 08:31:22 -04:00
|
|
|
rv = main()
|
|
|
|
sys.exit(rv)
|
2020-07-22 12:04:13 -04:00
|
|
|
|