From 8896e41726ebe14c6a134b7e759ee1b208405e20 Mon Sep 17 00:00:00 2001 From: alwayscompiling Date: Sat, 26 Nov 2022 23:20:35 -0500 Subject: [PATCH] Revert "Remove all data storage classes." This reverts commit 77a32b3adcf685f803078cddbc980e473377c63b. --- classes/category.py | 201 ++++++++++++++++++++++++++++++ classes/guild.py | 245 +++++++++++++++++++++++++++++++++++++ classes/highscores_data.py | 153 +++++++++++++++++++++++ classes/score.py | 154 +++++++++++++++++++++++ classes/table.py | 174 ++++++++++++++++++++++++++ 5 files changed, 927 insertions(+) create mode 100644 classes/category.py create mode 100644 classes/guild.py create mode 100644 classes/highscores_data.py create mode 100644 classes/score.py create mode 100644 classes/table.py diff --git a/classes/category.py b/classes/category.py new file mode 100644 index 0000000..cb00c5e --- /dev/null +++ b/classes/category.py @@ -0,0 +1,201 @@ +"""File for Category class.""" + +import json + +from classes.score import Score, ScoreTime # pylint: disable=import-error + + +class Category: + """Stores a category's information and relative scores.""" + + def __init__(self, name: str) -> None: + """ + Default constructor for Category class. Creates empty category and populates with defaults. + + Args: + name (str): Category's name + """ + self._name: str = name + self._ascending: bool = False + self._time_record: bool = False + self._scores_map: dict = {} + + def __str__(self) -> str: + """ + Returns Category class as a string for printing. + + Returns: + str: Category class as a string + """ + return self.to_str() + + @classmethod + def from_dict(cls, name: str, data: dict): + """ + Creates a Category class from the passed data. + + Args: + name (str): Category name + data (dict): Rest of Category data in dictionary format + + Returns: + Category: Populated Category object + """ + category = cls(name) + category.ascending = data["ascending"] + category.time_record = data["time_record"] + for user, score in data["scores"].items(): + if category.time_record: + score_obj: ScoreTime = ScoreTime(user) + score_obj.value = float(score) + category.add_score(score_obj) + else: + score_obj: Score = Score(user) + score_obj.value = int(score) + category.add_score(score_obj) + + return category + + @property + def ascending(self) -> bool: + """ + Getter for is ascending. + + Returns: + bool: is ascending + """ + return self._ascending + + @ascending.setter + def ascending(self, ascending: bool) -> None: + """ + Setter for is ascending. + + Args: + ascending (bool): is ascending + """ + self._ascending = ascending + + @property + def name(self) -> str: + """ + Getter for name. + + Returns: + str: name + """ + return self._name + + @property + def time_record(self) -> bool: + """ + Getter for is time record. + + Returns: + bool: is time record + """ + return self._time_record + + @time_record.setter + def time_record(self, time_record: bool) -> None: + """ + Setter for is time record. + + Args: + time_record (bool): is time record + """ + self._time_record = time_record + + @property + def scores(self) -> "list[Score]": + """ + Getter for Scores list. + + Returns: + list[Score]: List of Scores in this Category + """ + return self._scores_map.values() + + def add_score(self, score: Score) -> None: + """ + Adds score to dictionary storage map. Sorts dictionary by score value. + + Args: + score (Score): Score to be added + """ + if score.user in self._scores_map: + self._scores_map[score.user].value = score.value + else: + self._scores_map[score.user] = score + + self._scores_map = dict(sorted(self._scores_map.items(), key=lambda x: x[1].value, reverse=not self.ascending)) + + def clear(self) -> None: + """ + Clears all Scores tracked in score map. + This is a test function and shouldn't really be used. + """ + self._scores_map.clear() + + def get_users(self) -> "list[str]": + """ + Returns list of users in this category. + + Returns: + list[str]: List of all users in category. + """ + return self._scores_map.keys() + + def get_user_score(self, user: str) -> Score: + """ + Gets a user's score. Returns None if user is not found. + + Args: + user (str): Name of score to find + + Returns: + Score: User's Score or None + """ + if user in self._scores_map: + return self._scores_map[user] + return None + + def remove_score(self, user: str) -> Score: + """ + Removes a user's score from dictionary storage. + + Args: + user (str): user whose score to remove + + Returns: + Score: User's Score or None + """ + if user in self._scores_map: + return self._scores_map.pop(user) + return None + + def to_dict(self) -> dict: + """ + Saves category data as a python dictionary. + Does not include category name. It should be the key of above data structure. + + Returns: + dict: Category class in dictionary format + """ + data = {} + data["ascending"] = self.ascending + data["time_record"] = self.time_record + data["scores"] = {} + for score in self.scores: + data["scores"][score.user] = score.value + + return data + + def to_str(self) -> str: + """ + Returns Category object as a string. + + Returns: + str: string of json representation of Category + """ + return json.dumps(self.to_dict(), indent=4) diff --git a/classes/guild.py b/classes/guild.py new file mode 100644 index 0000000..bcdd4fe --- /dev/null +++ b/classes/guild.py @@ -0,0 +1,245 @@ +"""File for Guild class.""" + +import json + +from classes.table import Table # pylint: disable=import-error + + +class Guild: + """Stores a guild's information.""" + + def __init__(self, guild_id: int) -> None: + """ + Default constructor for Guild class. Initializes channel ids as 0 as they won't be + registered yet. Sets default username length of 12. Default highscores size to 3. + + Args: + guild_id (int): Discord Guild id of new Guild + """ + self._category_pad_length: int = 26 + self._guild_id: int = guild_id + self._highscore_channel_id: int = 0 + self._submission_channel_id: int = 0 + self._table_map: dict = {} + self._username_length: int = 12 + self._verification_channel_id: int = 0 + + def __str__(self) -> str: + """ + Returns Guild class as a string for printing. + + Returns: + str: Guild class as a string + """ + return self.to_str() + + @classmethod + def from_dict(cls, guild_id: int, data: dict): + """ + Creates a Guild class from the passed data. + + Args: + guild_id (int): Guild's id + data (dict): Rest of Guild data in dictionary format + + Returns: + Guild: Populated Guild object + """ + guild = cls(guild_id) + guild.category_pad_length = data["category_pad_length"] + guild.highscore_channel = data["highscore_channel_id"] + guild.submission_channel = data["submission_channel_id"] + guild.username_length = data["username_length"] + guild.verification_channel = data["verification_channel_id"] + + for table_name, table_data in data["tables"].items(): + table = Table.from_dict(table_name, table_data) + guild.add_table(table) + + return guild + + @property + def category_pad_length(self) -> int: + """ + Getter for Category Pad Length. + + Returns: + int: Length to pad each category entry. + """ + return self._category_pad_length + + @category_pad_length.setter + def category_pad_length(self, length: int) -> None: + """ + Setter for Category Pad Length. + + Args: + length (int): New length to set. + """ + self._category_pad_length = length + + @property + def guild_id(self) -> int: + """ + Getter for Guild id. + + Returns: + int: Guild id + """ + return self._guild_id + + @property + def highscore_channel(self) -> int: + """ + Getter for highscore channel id. + + Returns: + int: highscore channel id + """ + return self._highscore_channel_id + + @highscore_channel.setter + def highscore_channel(self, channel: int) -> None: + """ + Setter for highscore channel id. + + Args: + channel (int): highscore channel id + """ + self._highscore_channel_id = channel + + @property + def submission_channel(self) -> int: + """ + Getter for submission channel id. + + Returns: + int: submission channel id + """ + return self._submission_channel_id + + @submission_channel.setter + def submission_channel(self, channel: id) -> None: + """ + Setter fro submission channel id. + + Args: + channel (id): submission channel id + """ + self._submission_channel_id = channel + + @property + def tables(self) -> "list[Table]": + """ + Getter for Tables list. + + Returns: + list[Table]: List of Tables in this Guild + """ + return self._table_map.values() + + @property + def username_length(self) -> int: + """ + Getter for username length. + + Returns: + int: username length + """ + return self._username_length + + @username_length.setter + def username_length(self, length: int) -> None: + """ + Setter for username length. + + Args: + length (int): username length + """ + self._username_length = length + + @property + def verification_channel(self) -> int: + """ + Getter for verification channel id. + + Returns: + int: verification channel id + """ + return self._verification_channel_id + + @verification_channel.setter + def verification_channel(self, channel: int) -> None: + """ + Setter for verification channel id + + Args: + channel (int): verification channel id + """ + self._verification_channel_id = channel + + def add_table(self, table: Table) -> None: + """ + Adds a Table to table map. Shouldn't exist already. + + Args: + table (Table): new Table to add + """ + if table.name not in self._table_map: + self._table_map[table.name] = table + + def get_table(self, table_name: str) -> Table: + """ + Getter for specific Table. Returns None if Table is not found. + + Args: + table_name (str): Name of Table to find + + Returns: + Table: Table corresponding to Name. None if not found. + """ + if table_name in self._table_map: + return self._table_map[table_name] + return None + + def get_tables(self) -> dict: + """ + Getter for dict of table names and tables. + + Returns: + dict: Dictionary consisting of the Table Names and the Table object itself. + """ + + # I don't know if this is const data. Don't want to allow data to be written as a result of this function. + return self._table_map + + def to_dict(self) -> dict: + """ + Creates python dictionary object from class data. + Does not include guild_id in dict, it should be the key of above data structure. + + Returns: + dict: Guild class in dictionary format + """ + data: dict = {} + + data["category_pad_length"] = self.category_pad_length + data["highscore_channel_id"] = self.highscore_channel + data["submission_channel_id"] = self.submission_channel + data["tables"] = {} + data["username_length"] = self.username_length + data["verification_channel_id"] = self.verification_channel + + for table in self.tables: + data["tables"][table.name] = table.to_dict() + + return data + + def to_str(self) -> str: + """ + Returns Guild object as a string. + + Returns: + str: string of json representation of Guild + """ + return json.dumps(self.to_dict(), indent=4) diff --git a/classes/highscores_data.py b/classes/highscores_data.py new file mode 100644 index 0000000..51ce5cf --- /dev/null +++ b/classes/highscores_data.py @@ -0,0 +1,153 @@ +"""Class and functions for storing and retrieving highscores data.""" + +import os +import json + +from classes.guild import Guild # pylint: disable=import-error +from classes.table import Table # pylint: disable=import-error +from classes.category import Category # pylint: disable=import-error + + +HIGHSCORES_BACKUP_DIR = "deleted_backups" +HIGHSCORES_CONFIG_DIR = "example_config" +HIGHSCORES_DATA_DIR = "highscores_data" + + +class HighscoresData: + """Class for storing and retrieving highscores data.""" + + def __init__(self) -> None: + """ + Default constructor for Highscores Data. Opens up known highscores data on creation. + """ + self._guild_map: dict = {} + + # Ensure that data directories exist. + if not os.path.exists(HIGHSCORES_BACKUP_DIR): + os.mkdir(HIGHSCORES_BACKUP_DIR) + if not os.path.exists(HIGHSCORES_DATA_DIR): + os.mkdir(HIGHSCORES_DATA_DIR) + self.open_highscores_data() + + def __str__(self) -> str: + """ + Returns Highscores class as a string for printing. + Returns: + str: Highscores class as a string + """ + data: dict = {} + for guild_id, guild_obj in self._guild_map.items(): + data[guild_id] = guild_obj.to_dict() + return "Highscores Data\n" + json.dumps(data, indent=4) + + @property + def guilds(self) -> "list[Guild]": + """ + Getter for Guilds list. + + Returns: + list[Guild]: List of Guilds tracked by Highscores Bot + """ + return self._guild_map.values() + + def add_configuration(self, guild_id: int, config_str: str) -> None: + """ + Adds an example configuration to a Guild. Uses guild id to know which guild and + config name to open a file in example_config. + + Args: + guild_id (int): Guild id to add config to + config_str (str): Config name to add + """ + config = self._open_highscores_config(config_str) + guild = self.get_guild(guild_id) + + for table_name, categorys in config["tables"].items(): + table_obj = Table(table_name) + table_obj.size = config["table_size"] + for category_name in categorys: + category = Category(category_name) + category.ascending = config["categories"][category.name]["ascending"] + category.time_record = config["categories"][category.name]["is_time_record"] + table_obj.add_category(category) + + guild.add_table(table_obj) + + def add_guild(self, guild_id: int) -> None: + """ + Creates a new Guild and adds to Guild map. + + Args: + guild_id (int): Guild Id to add + """ + guild: Guild = Guild(guild_id) + self._guild_map[guild_id] = guild + + def get_guild(self, guild_id: int) -> Guild: + """ + Getter for Guild object that corresponds with the guild_id. + + Args: + guild_id (int): id of Guild to retrieve. + + Returns: + Guild: Guild object for guild_id + """ + return self._guild_map[guild_id] + + def remove_guild(self, guild_id: int) -> None: + """ + Removes a guild's data from object. Saves backup. + + Args: + guild_id (int): guild id to remove + """ + self._guild_map.pop(guild_id) + + os.rename( + f"{HIGHSCORES_DATA_DIR}/{guild_id}.json", f"{HIGHSCORES_BACKUP_DIR}/{guild_id}.json" + ) + + def _open_highscores_config(self, config: str) -> dict: + """ + Opens the highscores config file and returns in dictionary format. + Will be replaced when config class is created. + + Args: + config (str): Name of config to load. + + Returns: + dict: dict of highscores config + """ + with open(f"{HIGHSCORES_CONFIG_DIR}/{config}.json", encoding="utf-8", mode="r") as file: + return json.load(file) + + def open_highscores_data(self): + """ + Opens all saved highscores data in the HIGHSCORES_DATA_DIR dir. Creates a Guild object from + each file and adds to guild map. + """ + for data_file in os.listdir(HIGHSCORES_DATA_DIR): + guild_id: int = int(data_file.split(".", maxsplit=1)[0]) + with open(f"{HIGHSCORES_DATA_DIR}/{data_file}", encoding="utf-8", mode="r") as file: + guild_obj: Guild = Guild.from_dict(guild_id, json.load(file)) + self._guild_map[guild_obj.guild_id] = guild_obj + + def save_all_data(self) -> None: + """ + Calls save_guild_data for each guild tracked in guild map. + """ + for guild in self.guilds: + self.save_guild_data(guild) + + def save_guild_data(self, guild: Guild) -> None: + """ + Saves a guild to its own json file. + + Args: + guild (Guild): Guild to save + """ + with open( + f"{HIGHSCORES_DATA_DIR}/{guild.guild_id}.json", encoding="utf-8", mode="w" + ) as file: + file.write(guild.to_str()) # should use str function diff --git a/classes/score.py b/classes/score.py new file mode 100644 index 0000000..c6f4344 --- /dev/null +++ b/classes/score.py @@ -0,0 +1,154 @@ +"""File for Score classes""" + +import time + + +class Score: + """Parent class for Scores. Stores common information for a Score.""" + + # no user setter, it should never change. + + def __init__(self, user: str) -> None: + """ + Default constructor for Score class. + + Args: + user (str): Score's user + value (int): Score's value + """ + self._user: str = user + self._value: int = -1 + + def __str__(self) -> str: + """ + Returns Score class as a string for printing. + + Returns: + str: Score class as a string + """ + return self.to_str() + + @property + def user(self) -> str: + """ + Getter for score user. + + Returns: + str: user + """ + return self._user + + @user.setter + def user(self, username: str) -> None: + """ + Setter for score's username. + + Args: + username (str): username to set to + """ + self._user = username + + @property + def value(self) -> int: + """ + Getter for score value. + + Returns: + int: value + """ + return self._value + + @value.setter + def value(self, value: int) -> None: + """ + Setter for score value. + + Args: + value (int): score value input + """ + self._value = value + + def get_str(self) -> str: + """ + Gets score's value as string. + + Returns: + str: Score's value + """ + return str(self._value) + + def set_value_str(self, value: str) -> None: + """ + Setter for score value from string. + + Args: + value (str): int as a string + """ + self._value = int(value) + + def to_str(self) -> str: + """ + Returns Score object as a string. + + Returns: + str: string of json representation of Score + """ + return f"{self.user}: {self.get_str()}" + + +class ScoreTime(Score): + """Class for a time based score. Inherits from Score.""" + + def __init__(self, user: str) -> None: # pylint: disable=super-init-not-called + """ + Default constructor for ScoreTime class. + + Args: + user (str): Score's user + score (float): Score's value + """ + # store time score as seconds. + self._user: str = user + self._score: float = -1.0 + + @property + def value(self) -> float: + """ + Getter for score value. + + Returns: + float: value + """ + return self._score + + @value.setter + def value(self, value: float) -> None: + """ + Setter for score value. + + Args: + value (float): value + """ + self._score = value + + def get_str(self) -> str: + """ + Gets score's value as string. Turns seconds into MM:SS.ms format. + + Returns: + str: value in time format. + """ + milliseconds = round((self.value - int(self.value)) * 10) + return time.strftime(f"%-M:%S.{milliseconds}", time.gmtime(self.value)) + + def set_value_str(self, value: str) -> None: + """ + Setter for score value from a string. + + Args: + value (str): string representation of time + """ + if value.find(":"): + self._score = int(value.split(":")[0]) * 60 + float(value.split(":")[1]) + else: + self._score = float(value) diff --git a/classes/table.py b/classes/table.py new file mode 100644 index 0000000..e12224e --- /dev/null +++ b/classes/table.py @@ -0,0 +1,174 @@ +"""File for Table class.""" + +import json + +from classes.category import Category # pylint: disable=import-error + + +class Table: + """Stores a table's information a list of categories.""" + + def __init__(self, name: str) -> None: + """ + Default constructor for Table class. + Creates object and fills out class variables with defaults. + + Args: + name (str): Table's name + """ + self._name: str = name + self._message_id: int = 0 + self._categories_map: dict = {} + self._size = 3 + + def __str__(self) -> str: + """ + Returns Table class as a string for printing. + + Returns: + str: Table class as a string + """ + return self.to_str() + + @classmethod + def from_dict(cls, table_name: str, data: dict): + """ + Creates a Table class from the passed data. + + Args: + table_name (str): Table name + data (dict): Rest of Table data in dictionary format + + Returns: + Table: Populated Table object + """ + table = cls(table_name) + + table.message_id = data["message_id"] + table.size = data["table_size"] + + for cat_name, cat in data["categories"].items(): + category = Category.from_dict(cat_name, cat) + table.add_category(category) + + return table + + @property + def categories(self) -> "list[Category]": + """ + Getter for Categories list. + + Returns: + list[Category]: List of Categories in this Table + """ + return self._categories_map.values() + + @property + def message_id(self) -> int: + """ + Getter for message id + + Returns: + int: message id + """ + return self._message_id + + @message_id.setter + def message_id(self, m_id: int) -> None: + """ + Setter for message id. + + Args: + m_id (int): message id + """ + self._message_id = m_id + + @property + def name(self) -> str: + """ + Getter for name. + + Returns: + str: name + """ + return self._name + + @property + def size(self) -> int: + """ + Getter for size of table. + + Returns: + int: Size of table + """ + return self._size + + @size.setter + def size(self, size: int) -> None: + """ + Setter for size of table. + + Args: + size (int): New size of table + """ + self._size = size + + def add_category(self, category: Category) -> None: + """ + Adds a Category to categories map. Shouldn't exist already. + + Args: + category (Category): New Category to add + """ + if category.name not in self._categories_map: + self._categories_map[category.name] = category + + def get_categories(self) -> dict: + """ + Getter for dict of category names and categories. + + Returns: + dict: Dictionary consisting of Category names and Category objects. + """ + # I don't know if this is const data. Don't want to allow data to be written as a result of this function. + return self._categories_map + + def get_category(self, category_name: str) -> Category: + """ + Getter for specific category. Returns None if not found. + + Args: + category_name (str): name of category to return + + Returns: + Category: Category corresponding to name. None if not found. + """ + if category_name in self._categories_map: + return self._categories_map[category_name] + return None + + def to_dict(self) -> dict: + """ + Saves Table as a python dictionary. + Does not include table name. It should be the key of above data structure. + + Returns: + dict: Table class in dictionary format + """ + data = {} + data["message_id"] = self.message_id + data["table_size"] = self.size + data["categories"] = {} + for category in self.categories: + data["categories"][category.name] = category.to_dict() + + return data + + def to_str(self) -> str: + """ + Returns Table object as a string. + + Returns: + str: string of json representation of Table + """ + return json.dumps(self.to_dict(), indent=4)