ayon-core/igniter/user_settings.py

493 lines
13 KiB
Python

# -*- coding: utf-8 -*-
"""Package to deal with saving and retrieving user specific settings."""
import os
from datetime import datetime
from abc import ABCMeta, abstractmethod
import json
# disable lru cache in Python 2
try:
from functools import lru_cache
except ImportError:
def lru_cache(maxsize):
def max_size(func):
def wrapper(*args, **kwargs):
value = func(*args, **kwargs)
return value
return wrapper
return max_size
# ConfigParser was renamed in python3 to configparser
try:
import configparser
except ImportError:
import ConfigParser as configparser
import platform
import six
import appdirs
_PLACEHOLDER = object()
class OpenPypeSecureRegistry:
"""Store information using keyring.
Registry should be used for private data that should be available only for
user.
All passed registry names will have added prefix `OpenPype/` to easier
identify which data were created by OpenPype.
Args:
name(str): Name of registry used as identifier for data.
"""
def __init__(self, name):
try:
import keyring
except Exception:
raise NotImplementedError(
"Python module `keyring` is not available."
)
# hack for cx_freeze and Windows keyring backend
if platform.system().lower() == "windows":
from keyring.backends import Windows
keyring.set_keyring(Windows.WinVaultKeyring())
# Force "OpenPype" prefix
self._name = "/".join(("OpenPype", name))
def set_item(self, name, value):
# type: (str, 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
"""
import keyring
keyring.set_password(self._name, name, value)
@lru_cache(maxsize=32)
def get_item(self, name, default=_PLACEHOLDER):
"""Get value of sensitive item from system's keyring.
See also `Keyring module`_
Args:
name (str): Name of the item.
default (Any): Default value if item is not available.
Returns:
value (str): Value of the item.
Raises:
ValueError: If item doesn't exist and default is not defined.
.. _Keyring module:
https://github.com/jaraco/keyring
"""
import keyring
value = keyring.get_password(self._name, name)
if value:
return value
if default is not _PLACEHOLDER:
return default
# NOTE Should raise `KeyError`
raise ValueError(
"Item {}:{} does not exist in keyring.".format(self._name, name)
)
def delete_item(self, name):
# type: (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
"""
import keyring
self.get_item.cache_clear()
keyring.delete_password(self._name, name)
@six.add_metaclass(ABCMeta)
class ASettingRegistry():
"""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):
# type: (str) -> ASettingRegistry
super(ASettingRegistry, self).__init__()
self._name = name
self._items = {}
def set_item(self, name, value):
# type: (str, 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, value):
# type: (str, str) -> None
# Implement it
pass
def __setitem__(self, name, value):
self._items[name] = value
self._set_item(name, value)
def get_item(self, name):
# type: (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):
# type: (str) -> str
# Implement it
pass
def __getitem__(self, name):
return self._get_item(name)
def delete_item(self, name):
# type: (str) -> None
"""Delete item from settings registry.
Args:
name (str): Name of the item.
"""
self._delete_item(name)
@abstractmethod
def _delete_item(self, name):
# type: (str) -> None
"""Delete item from settings.
Note:
see :meth:`openpype.lib.user_settings.ARegistrySettings.delete_item`
"""
pass
def __delitem__(self, name):
del self._items[name]
self._delete_item(name)
class IniSettingRegistry(ASettingRegistry):
"""Class using :mod:`configparser`.
This class is using :mod:`configparser` (ini) files to store items.
"""
def __init__(self, name, path):
# type: (str, str) -> IniSettingRegistry
super(IniSettingRegistry, self).__init__(name)
# get registry file
version = os.getenv("OPENPYPE_VERSION", "N/A")
self._registry_file = os.path.join(path, "{}.ini".format(name))
if not os.path.exists(self._registry_file):
with open(self._registry_file, mode="w") as cfg:
print("# Settings registry", cfg)
print("# Generated by OpenPype {}".format(version), cfg)
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
print("# {}".format(now), cfg)
def set_item_section(
self, section, name, value):
# type: (str, str, 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, value):
# type: (str, str) -> None
self.set_item_section("MAIN", name, value)
def set_item(self, name, value):
# type: (str, 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):
# type: (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, name):
# type: (str, 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(
"Registry doesn't contain value {}:{}".format(section, name))
return value
def _get_item(self, name):
# type: (str) -> str
return self.get_item_from_section("MAIN", name)
def delete_item_from_section(self, section, name):
# type: (str, 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(
"Registry doesn't contain value {}:{}".format(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:`~openpype.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):
# type: (str, str) -> JSONSettingRegistry
super(JSONSettingRegistry, self).__init__(name)
#: str: name of registry file
self._registry_file = os.path.join(path, "{}.json".format(name))
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
header = {
"__metadata__": {
"openpype-version": os.getenv("OPENPYPE_VERSION", "N/A"),
"generated": now
},
"registry": {}
}
if not os.path.exists(os.path.dirname(self._registry_file)):
os.makedirs(os.path.dirname(self._registry_file), exist_ok=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):
# type: (str) -> object
"""Get item value from registry json.
Note:
See :meth:`openpype.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(
"Registry doesn't contain value {}".format(name))
return value
def get_item(self, name):
# type: (str) -> object
"""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, value):
# type: (str, object) -> None
"""Set item value to registry json.
Note:
See :meth:`openpype.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, value):
# type: (str, object) -> 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):
# type: (str) -> None
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 OpenPypeSettingsRegistry(JSONSettingRegistry):
"""Class handling OpenPype general settings registry.
Attributes:
vendor (str): Name used for path construction.
product (str): Additional name used for path construction.
"""
def __init__(self, name=None):
self.vendor = "pypeclub"
self.product = "openpype"
if not name:
name = "openpype_settings"
path = appdirs.user_data_dir(self.product, self.vendor)
super(OpenPypeSettingsRegistry, self).__init__(name, path)