Skip to content

Instantly share code, notes, and snippets.

@perryBunn
Last active April 16, 2022 16:07
Show Gist options
  • Save perryBunn/489a2edc595305e0312960af8fdcb7bc to your computer and use it in GitHub Desktop.
Save perryBunn/489a2edc595305e0312960af8fdcb7bc to your computer and use it in GitHub Desktop.
Eu4_cheeser
#! /usr/bin/env python3
"""
Description: This will run at a regular interval to make copies of the EU4 save
specified. Creating backups of Ironman games to fall back to.
"""
from datetime import datetime
from logging import Logger
from pathlib import Path
from shutil import copyfile
from time import sleep
from typing import Union
import logging
import platform
import re
class ColoredFormatter(logging.Formatter):
""" Logging colored formatter
Adapted from Alexandra Zaharia, who adapted
this post https://stackoverflow.com/a/56944256/3638629
"""
grey = '\x1b[38;21m'
blue = '\x1b[38;5;39m'
yellow = '\x1b[38;5;226m'
red = '\x1b[38;5;196m'
bold_red = '\x1b[31;1m'
reset = '\x1b[0m'
def __init__(self, fmt):
super().__init__()
self.fmt = fmt
self.formats = {
logging.DEBUG: self.grey + self.fmt + self.reset,
logging.INFO: self.blue + self.fmt + self.reset,
logging.WARNING: self.yellow + self.fmt + self.reset,
logging.ERROR: self.red + self.fmt + self.reset,
logging.CRITICAL: self.bold_red + self.fmt + self.reset
}
def format(self, record):
log_fmt = self.formats.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
def get_logger() -> Logger:
"""
Method will return a logging object with stdout and file handlers
Parameters:
None
Returns:
Logger: logging.Logger object
"""
log = logging.getLogger("eu4cheeser")
log.setLevel("DEBUG")
fmt = "%(asctime)s: %(name)s: %(levelname)s: %(message)s"
# Time formatted as YYYYmmddHHMM
date = datetime.now()
time = date.strftime("%Y%m%d%H%M")
log_dir = Path(f"LOGS/{time[:8]}")
log_dir.mkdir(exist_ok=True, parents=True)
file_name = (log_dir / f"{time}_cheeser.log").as_posix()
file_handle = logging.FileHandler(file_name, encoding="utf-8")
file_handle.setLevel("DEBUG")
stream_handle = logging.StreamHandler()
stream_handle.setLevel("INFO")
file_fmt = logging.Formatter(fmt)
stream_fmt = ColoredFormatter(fmt)
file_handle.setFormatter(file_fmt)
stream_handle.setFormatter(stream_fmt)
log.addHandler(stream_handle)
log.addHandler(file_handle)
return log
def get_response(question: str, valid_res: list=None, default=None, tries: int=1,
not_valid: list=None, permutations: bool=False) -> Union[None, str]:
""" Gets response to a question asked.
Parameters
----------
question: str
Question to be asked to the user.
valid_res: list
List of valid responses.
tries: int
Number of tries that the user has to answer the question.
not_valid: list
List of not valid responses.
permutations: bool
Creates permutations of valid responses. Useful for questions where
multiple responses could be possible/
Returns
-------
None | str
if an int is returned then the response from the user is not valid.
"""
if permutations and 2 <= len(valid_res):
upb = len(valid_res)
for i in range(0, upb - 1):
temp = valid_res[i]
for j in range(1, upb):
if i == j:
continue
temp+=valid_res[j]
valid_res.append(temp)
# Strips chars ' ' and ':' from the question
_question = question.strip()
_question = _question.strip(":")
quest = f"{_question}"
if valid_res:
quest += " ("
for res in valid_res:
quest += f"{res} "
quest = quest[:-1] # Remove trailing ' '
quest += ")"
if default:
quest += f" [{default}]"
for i in range(tries):
res = input(f"{quest}: ")
if not res.strip() and default:
res = default
if not_valid:
for npat in not_valid:
nmatch = re.search(npat, res)
if nmatch:
print(f"{res} is an invalid response.")
continue
if valid_res is None or res in valid_res:
return res
print(f"{res} is an invalid response.")
return None
def list_saves(path: Path, logger) -> (list, dict):
names = []
res = {}
game_paths = list(path.glob("*"))
if not len(game_paths) > 0:
logger.info("No save games found")
return None, None
print("EU4 Save Games:")
print("="*79)
for game in game_paths:
print(game.name)
res[game.name] = game
names.append(game.name)
return names, res
def which_os() -> Path:
system = platform.system()
if system == "Windows":
return Path("C:\\Users\\USER\\Documents\\Paradox Interactive\\Europa Universalis IV\\").resolve()
elif system == "Linux":
return Path("~/.local/share/Paradox Interactive/Europa Universalis IV/").expanduser()
elif system == "Darwin":
return Path("/Users/USER/Documents/Paradox Interactive/Europa Universalis IV/").resolve()
else:
raise RuntimeError("Unrecognized OS")
def make_backup(save: Path, date: str, logger: Logger):
logger.debug(f"Making backup of {save.name} at {date}")
backup_path = save.parent / f"{save.name[:-len(save.suffix)]}_cheeser"
copyfile(save, backup_path / f"{date}_{save.name}")
def main():
logger = get_logger()
save_path = which_os()
interval = int(get_response("Interval in minutes:"))*60
games, save_paths = list_saves(save_path / "save games", logger)
if games is None:
print("The program cannot continue without save games present. "
"Rerun the script once you have started a game.")
return
save_game = get_response("What save game do you want to watch?",
valid_res=games, tries=2)
save_game_path = save_paths[save_game]
try:
while True:
date = datetime.now().strftime("%Y%m%d%H%M")
try:
make_backup(save_game_path, date, logger)
except Exception as err:
logger.error(err)
logger.warning("Error encountered while trying to make backup. "
"Check logs for details.")
sleep(interval)
except KeyboardInterrupt:
print('interrupted!')
print("Stopping save interval. Bye!")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment