diff --git a/lokrez/__init__.py b/lokrez/__init__.py index 53bd6b3..a5478fb 100644 --- a/lokrez/__init__.py +++ b/lokrez/__init__.py @@ -1,10 +1,13 @@ import argparse import configparser +import copy import datetime import html +import io import json import logging import pathlib +import pprint import requests import sys import urllib @@ -31,6 +34,62 @@ LOG_DUMMY.addHandler(logging.NullHandler()) DEFAULT_DIR_TEMPLATES = ROOTDIR / "templates" +# ============================================================================= +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) + + # ============================================================================= def get_templates_list( dir_templates = DEFAULT_DIR_TEMPLATES, @@ -47,6 +106,76 @@ def get_templates_list( return templates_list +# ============================================================================= +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, + ) + # ============================================================================= def get_infos_from_url( url, @@ -65,25 +194,76 @@ def get_infos_from_url( if outform not in [ "dict", "lkrz" ]: raise ValueError("Unsupported outform") + # ------------------------------------------------------------------------- + tournament = None + event = None + # ------------------------------------------------------------------------- if url_parsed.netloc == "smash.gg": if (url_parsed.path.split("/")[1] != "tournament"): - raise Exception("No tournament found in url {}".format(url_parsed.path.split("/"))) + log.error("Incomplete URL '{}'".format(url)) + raise Exception("No tournament found in url {}".format(url)) - # Get infos from smash.gg and write the config file - tournament, top_players = getTournamentTop( - id_or_slug = url_parsed.path.split("/")[2], - import_options = options, - top = top, - token = token, - proxy = proxy, - log = log, - ) + try: + tournament = url_parsed.path.split("/")[2] + except: + log.error("Incomplete URL '{}'".format(url)) + raise Exception("No tournament slug found in url {}".format(url)) - 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") + 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, + ): # ------------------------------------------------------------------------- if outform == "dict": @@ -117,36 +297,24 @@ def init_resources( # Start resources download according to game if game in ssbu.GAME.list_names(): - resources.download_res( - dstdir = imgdir, - game = ssbu, - source = source, - store_raw = store_raw, - proxy = proxy, - log = log, - ) + game = ssbu elif game in melee.GAME.list_names(): - resources.download_res_ssbm( - dstdir = imgdir, - game = melee, - source = source, - store_raw = store_raw, - proxy = proxy, - log = log, - ) + game = melee elif game in pplus.GAME.list_names(): - resources.download_res( - dstdir = imgdir, - game = pplus, - source = source, - store_raw = store_raw, - proxy = proxy, - log = log, - ) + game = pplus else: log.error("Unknown game '{}'".format(game)) return 1 + resources.download_res( + dstdir = imgdir, + game = game, + source = source, + store_raw = store_raw, + proxy = proxy, + log = log, + ) + return 0 # ============================================================================= @@ -161,12 +329,20 @@ def generate_pic( log = LOG_DUMMY, ): + if outform.startswith("."): + outform = outform[1:] + if outform not in ["svg", "png"]: raise Exception("Unsupported outform") if type(infos_or_lkrzfile) == str: - # TODO : load lkrz as dict infos - raise NotImplementedError() + # load lkrz as dict infos + infos = get_infos_from_file( + lkrz_file_path = pathlib.Path(infos_or_lkrzfile), + options = options, + outform = "dict", + log = log, + ) else: infos = infos_or_lkrzfile @@ -185,7 +361,7 @@ def generate_pic( ), "dir_res_ssbu": dir_res, "dir_template": str(dir_templates/template), - "options": options.get("template_options", []), + "options": options, } pic = export.generate_pic( @@ -195,7 +371,7 @@ def generate_pic( outform, log = log, cachedir = dir_cache, - options = { "svg_embed_png": options.get("svg_embed_png",False) }, + options = options, ) if pic is None: @@ -308,21 +484,24 @@ def main(): ) top8_parser.add_argument( "--template-options", "-TO", - action = "append", - default = [], + nargs="+", + action = StoreOptionKeyPair, + default = {}, help = "Template-specific options (like 'covid' or 'animated')", ) top8_parser.add_argument( "--export-options", "-EO", - action = "append", - default = [], + nargs="+", + action = StoreOptionKeyPair, + default = {}, help = "Export options (like 'svg_embed_png')", ) top8_parser.add_argument( "--import-options", "-IO", - action = "append", - default = [], + nargs="+", + action = StoreOptionKeyPair, + default = {}, help = "Import options (like 'use_smashgg_prefixes')", ) @@ -378,6 +557,10 @@ def main(): log.addHandler(log_handler_console) + # Print all arguments in debug + # ------------------------------------------------------------------------- + log.debug( "Command arguments:\n{}".format( pprint.pformat(vars(args))) ) + # Print version if required # ------------------------------------------------------------------------- if args.version: @@ -423,58 +606,21 @@ def main(): # tournament = None top_players = {} + lkrz_file = None - if args.lkrz_file is not None and args.lkrz_file.exists(): - lkrz = configparser.ConfigParser() - lkrz.read(str(args.lkrz_file)) - - log.info("Loading data from '{}'".format(args.lkrz_file)) - - for s in lkrz: - section = lkrz[s] - if s == "Tournament": - tournament = smashgg.Tournament( - id = 0, - name = section["name"], - slug = section["slug"], - startAt = datetime.datetime.strptime( - section["date"], - "%Y-%m-%d %H:%M:%S", - ), - numEntrants = int(section["numEntrants"]), - venueName = section["location"], - ) - - 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, - ) - - else: + all_options = { + **args.import_options, + **args.template_options, + **args.export_options, + } + # Determine the nature of the 'tournament' argument : + # - url + # - id or slug + # - lkrz file + # url + if ( args.tournament.startswith("http://") + or args.tournament.startswith("https://") ): infos = get_infos_from_url( url = args.tournament, token = args.token, @@ -485,29 +631,33 @@ def main(): log = log, ) - tournament = infos["tournament"] - top_players = infos["players"] - - if tournament is None or top_players is None: - log.error("Could not load data from smash.gg") - return 1 - - # Save a lkrz file - lkrz_data = "\n".join( - [ tournament.conf() ] \ - + list(map( - lambda p:p.conf(), - top_players, - )) + # 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, ) - if args.lkrz_file is None: - args.lkrz_file = pathlib.Path( - "{}.lkrz".format(tournament.slug) - ) + # 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, + ) - with args.lkrz_file.open("w", encoding="utf8") as f: - f.write(lkrz_data) + tournament = infos["tournament"] + top_players = infos["players"] + + if tournament is None or top_players is None: + log.error("Could not load data") + return 1 # Default outfile is 'tournament-slug.svg' if args.outfile is None: @@ -515,48 +665,60 @@ def main(): "{}.svg".format(tournament.slug), ) - # Build the context which will be passed to the template + # 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 try: - dir_res_ssbu = args.imgdir.as_uri() # not absolute => error + dir_res = (args.imgdir / tournament.game.name).as_uri() # not absolute => error except ValueError: - dir_res_ssbu = args.imgdir.as_posix() + dir_res = (args.imgdir / tournament.game.name).as_posix() - context = { - "tournament": tournament.clean_name(args.name_seo_delimiter), - "players" : sorted( - top_players, - key = lambda p: p.placement, - ), - "dir_res_ssbu": dir_res_ssbu, - "dir_template": str(args.templatesdir / args.template), - "options": args.template_options, - } - - rv = export.generate_outfile( - args.templatesdir, - args.template, - context, - args.outfile, + 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, log = log, - cachedir = args.cachedir, - options={"svg_embed_png": "svg_embed_png" in args.export_options}, ) - if rv is None: + if pic is None: return 1 - log.info("Successfully saved outfile as '{}'".format(rv)) + 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()) return 0 # ----------------------------------------------------------------------------- def getTournamentTop( id_or_slug, - import_options = {} + event_slug = None, + import_options = [], top = 8, token = "", proxy = None, - log=LOG_DUMMY): + log=LOG_DUMMY, + ): """Returns a tuple : the smashgg.Tournament object and a list of the top smashgg.Player in that tournament.""" @@ -605,6 +767,36 @@ def getTournamentTop( return event + # ------------------------------------------------------------------------- + # 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 + # ------------------------------------------------------------------------- data = None @@ -644,7 +836,11 @@ def getTournamentTop( log.error("Failed to load Tournament") return None,None - event = selectBiggestEvent(tournament_data, log) + event = None + if event_slug is None: + event = selectBiggestEvent(tournament_data, log) + else: + event = selectEventBySlug(tournament_data, event_slug, log) if event is None : return None,None @@ -653,6 +849,7 @@ def getTournamentTop( tournament = smashgg.Tournament( id = tournament_data["id"], slug = tournament_data["slug"], + game = event["videogame"], name = tournament_data["name"], startAt = \ datetime.datetime. \ @@ -663,7 +860,10 @@ def getTournamentTop( city = tournament_data["city"], countryCode = tournament_data["countryCode"], hashtag = tournament_data["hashtag"], - ) + ) \ + .clean_name( + import_options.get("name_seo_delimiter", None) + ) # Get the top players @@ -688,10 +888,10 @@ def getTournamentTop( ["authorizations"] \ [0] \ ["externalUsername"] - except TypeError: + except: twitterHandle = None - if import_options.get("use_smashgg_prefixes", True): + if "use_smashgg_prefixes" in import_options: prefix = participant_data["prefix"] else: prefix = "" @@ -733,7 +933,11 @@ def getTournamentTop( log.error("Failed to load Tournament") return None,None - event = selectBiggestEvent(tournament_data, log) + event = None + if event_slug is None: + event = selectBiggestEvent(tournament_data, log) + else: + event = selectEventBySlug(tournament_data, event_slug, log) if event is None : return None,None @@ -753,6 +957,7 @@ def getTournamentTop( eid = slct["entrant"]["id"] try: top_players[eid].add_character_selection( + game = tournament.game, character = slct["selectionValue"], win = (winnerId == eid), )