Source code for research_client.utils

"""Utility functions for the LART Research Client app."""
import os
import shutil
import logging
import json
from pathlib import Path
from tkinter import Tk, Label, filedialog, messagebox
from typing import Any, Literal
from .config import config, Config, _default_paths                               # type: ignore

logger = logging.getLogger(__name__)


[docs]def manage_settings(command: Literal["update", "reset", "clear"] | str) -> bool: # noqa: C901 """Manage app settings file. Args: command: One of the operations to be carried out on the settings file, or a JSON string with key-value pairs to be merged into the current settings for the app. The following commands are available by keyword: - `update`: Load app settings from current file and save them again. This is useful if a settings file may not include all the key-value pairs that a user may want to control, for instance after an app update. - `reset`: Reset the settings file to the hard-coded app defaults. This is useful in cases where a settings file may have become corrupted and where the user wants to start afresh with manually edit the local settings. Effectively, this is the same as using `clear` followed by `update`. - `clear`: Remove the settings file. On the next start-up, the app will then use the hard-coded app defaults. This is useful in cases where the user wants to revert to the apps default settings, without the intent to make manual changes. """ config_file = _default_paths.user_config_path / "settings.json" if command == "clear": logger.debug("Clearing the settings file...") if config_file.is_file(): config_file.unlink() if not config_file.is_file(): logger.info(f"Successfully deleted settings file '{config_file!s}'") return True else: logger.error(f"Failed to delete settings file at '{config_file!s}'") return False logger.info(f"No settings file found at '{config_file!s}'") return True if command == "update": logger.debug("Updating the settings file...") if config_file.exists(): loaded = Config.load("settings.json") logger.debug(f"Loaded settings from file '{config_file!s}'.") if not manage_settings("clear"): return False else: loaded = Config() logger.debug(f"No settings file found at {config_file!s}, using app defaults") loaded.save("settings.json") if config_file.exists(): logger.info(f"Successfully updated config file at '{config_file!s}'") return True logger.error(f"Failed to save settings file to '{config_file!s}'") return False if command == "reset": logger.debug("Resetting the settings file...") if not manage_settings("clear"): return False if not manage_settings("update"): return False logger.info(f"Successfully restored defaults to settings file at {config_file!s}") return True if command.startswith("{") and command.endswith("}"): logger.debug(f"Updating settings with supplied JSON string: {command}") data: Any = json.loads(command) loaded = Config.load("settings.json") stored_attrs: list[str] = [] unknown_attrs: list[Any] = [] if isinstance(data, dict): for key, value in data.items(): # type: ignore if isinstance(key, str) and _recursively_overwrite_attr(loaded, key, value): stored_attrs.append(key) else: unknown_attrs.append(key) else: logger.error("Supplied JSON string is not a valid dict of key-value pairs.") return False logger.info(f"Successfully modified settings: {stored_attrs!r}") if unknown_attrs: logger.error(f"Failed to modify unknown settings: {unknown_attrs!r}") loaded.save("settings.json") logger.info(f"Successfully stored modified settings file at {config_file!s}") return True logger.error(f"Unrecognised settings management command: '{command}'") return False
[docs]def _recursively_overwrite_attr(obj: object, attr: str, value: Any) -> bool: subattr: str | None = None if "." in attr: tmp = attr.split(".", 1) attr = tmp[0] subattr = tmp[1] if hasattr(obj, attr): if subattr and isinstance(getattr(obj, attr), object): if _recursively_overwrite_attr(getattr(obj, attr), subattr, value): return True if not subattr: setattr(obj, attr, value) return True return False
[docs]def export_backup(filename: Path | str | None = None) -> bool: """Export app data as a ZIP archive. Prompt for filename if needed.""" logger.debug("Exporting data backup...") if filename is None: tkroot = Tk() tkroot.title("LART Research Client data backup") if os.name == "nt": tkroot.iconbitmap( # type: ignore str(Path(__file__).parent / "web" / "img" / "appicon.ico") ) label = Label( master=tkroot, text="Please select the path to save the data backup to...", font=("Helvetica 13") ) label.pack() tkroot.geometry("500x50") tkroot.lift() tkroot.withdraw() from datetime import datetime dialog = filedialog.SaveAs( master=tkroot, title="Save Data Backup as...", initialfile=datetime.now().strftime("lartrc_backup_%Y-%m-%dT%H%M%S.zip"), filetypes=[("ZIP Archives", "*.zip")], # takefocus="initialfile", ) filename = str(dialog.show()) # type: ignore if not filename: logger.error("No filename provided.") return False filename = Path(filename) logger.debug(f"Backup filename: '{filename}'") if str(filename).endswith(".zip"): filename = filename.with_suffix("") old_wd = Path.cwd() os.chdir(config.paths.data) result = shutil.make_archive(str(filename), "zip", logger=logger) os.chdir(old_wd) if Path(result).exists(): logger.info(f"Backup saved to file '{result}'.") return True logger.info("Failed to create backup.") return False
[docs]def show_error_dialog(title: str | None = None, message: str | None = None): """Display a graphical error message box even if eel is not active.""" tkroot = Tk() tkroot.withdraw() messagebox.showerror( title if title else "Error", message if message else "An unknown error occured." )