# -*- coding: utf-8 -*- """Package to deal with saving and retrieving user specific settings.""" import os from datetime import datetime from abc import ABC, abstractmethod import configparser import json from typing import Any from functools import lru_cache from pathlib import Path import appdirs import keyring from ..version import __version__ class ASettingRegistry(ABC): """Abstract class defining structure of **SettingRegistry** class. It is implementing methods to store secure items into keyring, otherwise mechanism for storing common items must be implemented in abstract methods. Attributes: _name (str): Registry names. """ def __init__(self, name: str): super(ASettingRegistry, self).__init__() self._name = name self._items = {} def set_item(self, name: str, value: str) -> None: """Set item to settings registry. Args: name (str): Name of the item. value (str): Value of the item. """ self._set_item(name, value) @abstractmethod def _set_item(self, name: str, value: str) -> None: # Implement it pass def __setitem__(self, name, value): self._items[name] = value self._set_item(name, value) def get_item(self, name: str) -> str: """Get item from settings registry. Args: name (str): Name of the item. Returns: value (str): Value of the item. Raises: ValueError: If item doesn't exist. """ return self._get_item(name) @abstractmethod def _get_item(self, name: str) -> str: # Implement it pass def __getitem__(self, name): return self._get_item(name) def delete_item(self, name: str): """Delete item from settings registry. Args: name (str): Name of the item. Returns: value (str): Value of the item. """ self._delete_item(name) @abstractmethod def _delete_item(self, name: str): """Delete item from settings. Note: see :meth:`pype.lib.user_settings.ARegistrySettings.delete_item` """ pass def __delitem__(self, name): del self._items[name] self._delete_item(name) def set_secure_item(self, name: str, value: str) -> None: """Set sensitive item into system's keyring. This uses `Keyring module`_ to save sensitive stuff into system's keyring. Args: name (str): Name of the item. value (str): Value of the item. .. _Keyring module: https://github.com/jaraco/keyring """ keyring.set_password(self._name, name, value) @lru_cache(maxsize=32) def get_secure_item(self, name: str) -> str: """Get value of sensitive item from system's keyring. See also `Keyring module`_ Args: name (str): Name of the item. Returns: value (str): Value of the item. Raises: ValueError: If item doesn't exist. .. _Keyring module: https://github.com/jaraco/keyring """ value = keyring.get_password(self._name, name) if not value: raise ValueError( f"Item {self._name}:{name} does not exist in keyring.") return value def delete_secure_item(self, name: str) -> None: """Delete value stored in system's keyring. See also `Keyring module`_ Args: name (str): Name of the item to be deleted. .. _Keyring module: https://github.com/jaraco/keyring """ self.get_secure_item.cache_clear() keyring.delete_password(self._name, name) class IniSettingRegistry(ASettingRegistry): """Class using :mod:`configparser`. This class is using :mod:`configparser` (ini) files to store items. """ def __init__(self, name, path: str): super(IniSettingRegistry, self).__init__(name) # get registry file self._registry_file = os.path.join(path, f"{name}.ini") if not os.path.exists(self._registry_file): with open(self._registry_file, mode="w") as cfg: print("# Settings registry", cfg) print(f"# Generated by Pype {__version__}", cfg) now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") print(f"# {now} ") def set_item_section( self, section: str, name: str, value: str) -> None: """Set item to specific section of ini registry. If section doesn't exists, it is created. Args: section (str): Name of section. name (str): Name of the item. value (str): Value of the item. """ value = str(value) config = configparser.ConfigParser() config.read(self._registry_file) if not config.has_section(section): config.add_section(section) current = config[section] current[name] = value with open(self._registry_file, mode="w") as cfg: config.write(cfg) def _set_item(self, name: str, value: str) -> None: self.set_item_section("MAIN", name, value) def set_item(self, name: str, value: str) -> None: """Set item to settings ini file. This saves item to ``DEFAULT`` section of ini as each item there must reside in some section. Args: name (str): Name of the item. value (str): Value of the item. """ # this does the some, overridden just for different docstring. # we cast value to str as ini options values must be strings. super(IniSettingRegistry, self).set_item(name, str(value)) def get_item(self, name: str) -> str: """Gets item from settings ini file. This gets settings from ``DEFAULT`` section of ini file as each item there must reside in some section. Args: name (str): Name of the item. Returns: str: Value of item. Raises: ValueError: If value doesn't exist. """ return super(IniSettingRegistry, self).get_item(name) @lru_cache(maxsize=32) def get_item_from_section(self, section: str, name: str) -> str: """Get item from section of ini file. This will read ini file and try to get item value from specified section. If that section or item doesn't exist, :exc:`ValueError` is risen. Args: section (str): Name of ini section. name (str): Name of the item. Returns: str: Item value. Raises: ValueError: If value doesn't exist. """ config = configparser.ConfigParser() config.read(self._registry_file) try: value = config[section][name] except KeyError: raise ValueError( f"Registry doesn't contain value {section}:{name}") return value def _get_item(self, name: str) -> str: return self.get_item_from_section("MAIN", name) def delete_item_from_section(self, section: str, name: str) -> None: """Delete item from section in ini file. Args: section (str): Section name. name (str): Name of the item. Raises: ValueError: If item doesn't exist. """ self.get_item_from_section.cache_clear() config = configparser.ConfigParser() config.read(self._registry_file) try: _ = config[section][name] except KeyError: raise ValueError( f"Registry doesn't contain value {section}:{name}") config.remove_option(section, name) # if section is empty, delete it if len(config[section].keys()) == 0: config.remove_section(section) with open(self._registry_file, mode="w") as cfg: config.write(cfg) def _delete_item(self, name): """Delete item from default section. Note: See :meth:`~pype.lib.IniSettingsRegistry.delete_item_from_section` """ self.delete_item_from_section("MAIN", name) class JSONSettingRegistry(ASettingRegistry): """Class using json file as storage.""" def __init__(self, name, path: str): super(JSONSettingRegistry, self).__init__(name) #: str: name of registry file self._registry_file = Path(os.path.join(path, f"{name}.json")) now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") header = { "__metadata__": { "pype-version": __version__, "generated": now }, "registry": {} } self._registry_file.parent.mkdir(parents=True) if not os.path.exists(self._registry_file): with open(self._registry_file, mode="w") as cfg: json.dump(header, cfg, indent=4) @lru_cache(maxsize=32) def _get_item(self, name: str) -> Any: """Get item value from registry json. Note: See :meth:`pype.lib.JSONSettingRegistry.get_item` """ with open(self._registry_file, mode="r") as cfg: data = json.load(cfg) try: value = data["registry"][name] except KeyError: raise ValueError( f"Registry doesn't contain value {name}") return value def get_item(self, name: str) -> Any: """Get item value from registry json. Args: name (str): Name of the item. Returns: value of the item Raises: ValueError: If item is not found in registry file. """ return self._get_item(name) def _set_item(self, name: str, value: Any) -> None: """Set item value to registry json. Note: See :meth:`pype.lib.JSONSettingRegistry.set_item` """ with open(self._registry_file, "r+") as cfg: data = json.load(cfg) data["registry"][name] = value cfg.truncate(0) cfg.seek(0) json.dump(data, cfg, indent=4) def set_item(self, name: str, value: Any) -> None: """Set item and its value into json registry file. Args: name (str): name of the item. value (Any): value of the item. """ self._set_item(name, value) def _delete_item(self, name: str): self._get_item.cache_clear() with open(self._registry_file, "r+") as cfg: data = json.load(cfg) del data["registry"][name] cfg.truncate(0) cfg.seek(0) json.dump(data, cfg, indent=4) class PypeSettingsRegistry(JSONSettingRegistry): """Class handling Pype general settings registry. Attributes: vendor (str): Name used for path construction. product (str): Additional name used for path construction. """ vendor = "pypeclub" product = "pype" def __init__(self): path = appdirs.user_data_dir(self.product, self.vendor) super(PypeSettingsRegistry, self).__init__("pype_settings", path)