SecretBot/GameFiles/GamesFile.py

122 lines
4.3 KiB
Python

import atexit
import json
import logging
from pathlib import Path
from typing import Union, Dict, Callable, Tuple
import discord
from .Game import Game
logger = logging.getLogger(__name__)
# Functions used to convert between configuration versions, in a dictionary of: old_version -> converter
# Each converter takes the old configuration and returns the converted configuration and the new version
config_version_converters: Dict[Union[None, str], Callable[[Dict], Tuple[Dict, str]]] = {
}
class GamesFile:
"""
Wrapper around the configuration file for all games of all guilds
"""
version = "1.0"
@staticmethod
def empty_config() -> Dict:
"""
:return: An empty configuration, used to initialise the configuration file
"""
return {
"__version__": GamesFile.version,
}
def __init__(self, games_file_path: Union[str, Path]):
self.file_obj = None # File descriptor to the on-disk config file
self.config = None # In-memory configuration
# Loading configuration from disk
logger.debug("Loading games file")
games_file_path = Path(games_file_path).absolute()
if games_file_path.exists():
if not games_file_path.is_file():
raise ValueError(f"Games file {games_file_path} exists but is not a regular file")
self.file_obj = games_file_path.open("r+")
if games_file_path.stat().st_size == 0: # Config file is empty
logger.warning(f"games file {games_file_path} is empty, initialising an empty configuration")
self.config = self.empty_config()
self.save_to_file() # Initialise the file
else: # File is not empty
has_been_converted = self.reload_from_disk()
if has_been_converted:
self.save_to_file()
else: # File does not exist
self.file_obj = games_file_path.open("w+")
self.config = self.empty_config()
self.save_to_file() # Initialise the file
# Verifying that attributes have been initialised properly
assert self.file_obj is not None and self.config is not None
logger.debug("Games file successfully initialised")
atexit.register(self.save_to_file)
def reload_from_disk(self) -> bool:
"""
Reload the configuration from disk
:return: Whether the configuration had to be converted to a more recent version because the one on disk was an earlier version
:except json.JSONDecodeError: if file is not valid json
:except ValueError: if the version of the configuration in the file is not known or can not be converted to a more recent one
"""
logger.debug("Loading games file from disk")
self.file_obj.seek(0) # Moving to beginning of file
try:
self.config = json.load(self.file_obj)
except json.JSONDecodeError as e:
logger.critical(f"JSON Error when parsing games file: {e}")
raise e
# Checking configuration version and converting if needed
config_version = self.config["__version__"]
has_been_converted = False
if config_version != self.version:
logger.info(f"Games file is an older version, converting (file version: {config_version}, current: {self.version})...")
# Performing the conversion
while config_version != self.version:
if config_version in config_version_converters:
logger.debug(f"Converting from {config_version}")
self.config, config_version = config_version_converters[config_version](self.config)
logger.debug(f"Converted to {config_version}")
else:
logger.critical(f"Impossible to find converter to convert from {config_version}")
raise ValueError(f"Configuration loading: impossible to convert to current version")
has_been_converted = True
assert self.config["__version__"] == self.version
logger.debug("Loaded games file from disk")
return has_been_converted
def save_to_file(self, indent = 2) -> None:
"""
Save the configuration to disk
:param indent: Indentation to use for pretty-printing json
"""
logger.debug("Writing games file to disk")
self.file_obj.seek(0)
self.file_obj.truncate()
json.dump(self.config, self.file_obj, indent = indent)
self.file_obj.flush()
logger.debug("Written games file to disk")
def __getitem__(self, guild: discord.Guild) -> Game:
"""
Get the game information for one guild
"""
guild_id_str = str(guild.id)
if guild_id_str not in self.config:
self.config[guild_id_str] = Game.new_dict()
self.save_to_file()
return Game(self.config[guild_id_str], self.save_to_file)