diff --git a/config.py b/config.py index d90826e..14e1dc3 100644 --- a/config.py +++ b/config.py @@ -1,8 +1,11 @@ import shutil +from typing import Dict import yaml import platformdirs from pathlib import Path +from datetime import datetime, timezone +import dateutil.parser import constants from log_setup import logger @@ -71,3 +74,115 @@ def create_db_config() -> None: logger.info("Created default DB config file at {}".format(config_path)) else: logger.info("DB config file at {} already exists".format(config_path)) + + +def check_config_attr(func): + def wrapper(): + try: + func() + except KeyError as e: + logger.error("Missing config attribute:\n{}".format(e)) + + +class Config: + def __init__(self, config: Dict): + self._config = config + + @check_config_attr + def player_base_rating(self) -> int: + return self._config["player_base_rating"] + + @check_config_attr + def min_player_count(self) -> int: + return self._config["min_player_count"] + + @check_config_attr + def max_player_count(self) -> int: + return self._config["max_player_count"] + + @check_config_attr + def min_suit_count(self) -> int: + return self._config["min_suit_count"] + + @check_config_attr + def max_suit_count(self) -> int: + return self._config["max_suit_count"] + + @check_config_attr + def starting_game_id(self) -> int: + return self._config["starting_game_id"] + + @check_config_attr + def ending_game_id(self) -> int: + return self._config["ending_game_id"] + + @check_config_attr + def starting_time(self): + time = self._config["starting_time"] + return dateutil.parser(time, tzinfos={'EST': 'US/Eastern'}) + + @check_config_attr + def ending_time(self): + time = self._config["ending_time"] + return dateutil.parser(time, tzinfos={'EST': 'US/Eastern'}) + + @check_config_attr + def variant_base_rating(self, variant_name: str, player_count: int) -> int: + global_base_rating = self._config["variant_base_rating"] + + # We use different ways of specifying base ratings here: + # First, there is a (required) config setting for the variant base rating, which will be used as default. + # Then, for each variant, it is possible to either specify some base rating directly, + # or further specify a base rating for each player count. + # Parsing this is now quite easy: We just check if there is an entry for the specific variant and if so, + # read the base rating from there, where we will have to distinguish between a player-independent value + # and possibly player-specific entries. Whenever we don't find an explicit entry, we use the global fallback. + # This makes it possible to just specify the variant + player combinations that differ in their base rating + # from the globally specified one. + + var_rating = self._config.get("variant_base_ratings", {}).get(variant_name, None) + + if type(var_rating) == int: + return var_rating + elif type(var_rating) == dict: + return var_rating.get("{}p".format(player_count), global_base_rating) + elif var_rating is None: + return global_base_rating + + logger.error("Unexpected config format for entry {} in 'variant_base_ratings'".format(variant_name)) + + +def get_config_path(): + config_dir = Path(platformdirs.user_config_dir(constants.APP_NAME, ensure_exists=True)) + config_path = config_dir / constants.CONFIG_FILE_NAME + return config_path + + +def read_config() -> Config: + config_path = get_config_path() + logger.verbose("Hanabi League configuration read from file {}".format(config_path)) + if config_path.exists(): + with open(config_path, "r") as f: + config = yaml.safe_load(f) + return Config(config) + else: + logger.info("No hanabi league configuration found. Falling back to default file {}".format( + constants.DEFAULT_CONFIG_PATH)) + logger.info( + "Note: To turn off this message, create a config file at {}".format(config_path) + ) + with open(constants.DEFAULT_CONFIG_PATH, "r") as f: + config = yaml.safe_load(f) + return Config(config) + + +def create_config() -> None: + """ + Creates a default config file for the league at the config location + """ + config_path = get_config_path() + if not config_path.exists(): + shutil.copy(constants.DEFAULT_CONFIG_PATH, config_path) + logger.info("Created default hanabi league config file at {}".format(config_path)) + else: + logger.info("Hanabi league config file at {} already exists".format(config_path)) diff --git a/constants.py b/constants.py index 5d523f0..f205ada 100644 --- a/constants.py +++ b/constants.py @@ -3,7 +3,8 @@ # It's not meant to include all string constants or anything, just the ones that are important for functioning. APP_NAME = 'hanabi-league' -DB_CONFIG_FILE_NAME = 'config.yaml' +DB_CONFIG_FILE_NAME = 'db_config.yaml' +CONFIG_FILE_NAME = 'config.yaml' DEFAULT_DB_NAME = 'hanabi-league' DEFAULT_DB_USER = 'hanabi-league' @@ -27,3 +28,4 @@ DB_TABLE_NAMES = [ DATABASE_SCHEMA_PATH = 'install/database_schema.sql' DEFAULT_DB_CONFIG_PATH = 'install/default_db_config.yaml' +DEFAULT_CONFIG_PATH = 'install/default_config.yaml' diff --git a/install/default_config.yaml b/install/default_config.yaml new file mode 100644 index 0000000..2425c8b --- /dev/null +++ b/install/default_config.yaml @@ -0,0 +1,27 @@ +player_base_rating: 1500 +variant_base_rating: 1500 +variant_base_ratings: + No Variant: + 3p: 1500 + 4p: 1500 + 5p: 1500 + 6 Suits: + 3p: 1500 + 4p: 1500 + 5p: 1500 + Clue Starved: + 3p: 1500 + 4p: 1500 + 6p: 1700 + Clue Starved (6 Suits): + 3p: 1500 + 4p: 1500 + 6p: 1700 +min_player_count: 3 +max_player_count: 5 +min_suits: 5 +max_suits: 6 +starting_game_id: 1000000 +ending_game_id: 9999999 +starting_time: "2023-10-10 00:00:00 EST" +ending_time: "2023-12-10 00:00:00 EST" diff --git a/main.py b/main.py index f588503..59c3789 100644 --- a/main.py +++ b/main.py @@ -32,6 +32,7 @@ def subcommand_init(force: bool): def subcommand_generate_config(): config.create_db_config() + config.create_config() def get_parser() -> argparse.ArgumentParser: diff --git a/requirements.txt b/requirements.txt index fbc6dbf..3d6d08a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ psycopg2 platformdirs PyYAML verboselogs +python-dateutil