ayon-core/openpype/settings/handlers.py

1822 lines
58 KiB
Python

import os
import json
import copy
import collections
import datetime
from abc import ABCMeta, abstractmethod
import six
import openpype.version
from openpype.client.mongo import OpenPypeMongoConnection
from openpype.client.entities import get_project_connection, get_project
from .constants import (
GLOBAL_SETTINGS_KEY,
SYSTEM_SETTINGS_KEY,
PROJECT_SETTINGS_KEY,
PROJECT_ANATOMY_KEY,
LOCAL_SETTING_KEY,
M_OVERRIDDEN_KEY,
LEGACY_SETTINGS_VERSION
)
class SettingsStateInfo:
"""Helper state information about some settings state.
Is used to hold information about last saved and last opened UI. Keep
information about the time when that happened and on which machine under
which user and on which openpype version.
To create currrent machine and time information use 'create_new' method.
"""
timestamp_format = "%Y-%m-%d %H:%M:%S.%f"
def __init__(
self,
openpype_version,
settings_type,
project_name,
timestamp,
hostname,
hostip,
username,
system_name,
local_id
):
self.openpype_version = openpype_version
self.settings_type = settings_type
self.project_name = project_name
timestamp_obj = None
if timestamp:
timestamp_obj = datetime.datetime.strptime(
timestamp, self.timestamp_format
)
self.timestamp = timestamp
self.timestamp_obj = timestamp_obj
self.hostname = hostname
self.hostip = hostip
self.username = username
self.system_name = system_name
self.local_id = local_id
def copy(self):
return self.from_data(self.to_data())
@classmethod
def create_new(
cls, openpype_version, settings_type=None, project_name=None
):
"""Create information about this machine for current time."""
from openpype.lib.pype_info import get_workstation_info
now = datetime.datetime.now()
workstation_info = get_workstation_info()
return cls(
openpype_version,
settings_type,
project_name,
now.strftime(cls.timestamp_format),
workstation_info["hostname"],
workstation_info["hostip"],
workstation_info["username"],
workstation_info["system_name"],
workstation_info["local_id"]
)
@classmethod
def from_data(cls, data):
"""Create object from data."""
return cls(
data["openpype_version"],
data["settings_type"],
data["project_name"],
data["timestamp"],
data["hostname"],
data["hostip"],
data["username"],
data["system_name"],
data["local_id"]
)
def to_data(self):
data = self.to_document_data()
data.update({
"openpype_version": self.openpype_version,
"settings_type": self.settings_type,
"project_name": self.project_name
})
return data
@classmethod
def create_new_empty(cls, openpype_version, settings_type=None):
return cls(
openpype_version,
settings_type,
None,
None,
None,
None,
None,
None,
None
)
@classmethod
def from_document(cls, openpype_version, settings_type, document):
document = document or {}
project_name = document.get("project_name")
last_saved_info = document.get("last_saved_info")
if last_saved_info:
copy_last_saved_info = copy.deepcopy(last_saved_info)
copy_last_saved_info.update({
"openpype_version": openpype_version,
"settings_type": settings_type,
"project_name": project_name,
})
return cls.from_data(copy_last_saved_info)
return cls(
openpype_version,
settings_type,
project_name,
None,
None,
None,
None,
None,
None
)
def to_document_data(self):
return {
"timestamp": self.timestamp,
"hostname": self.hostname,
"hostip": self.hostip,
"username": self.username,
"system_name": self.system_name,
"local_id": self.local_id,
}
def __eq__(self, other):
if not isinstance(other, SettingsStateInfo):
return False
if other.timestamp_obj != self.timestamp_obj:
return False
return (
self.openpype_version == other.openpype_version
and self.hostname == other.hostname
and self.hostip == other.hostip
and self.username == other.username
and self.system_name == other.system_name
and self.local_id == other.local_id
)
@six.add_metaclass(ABCMeta)
class SettingsHandler(object):
global_keys = {
"openpype_path",
"admin_password",
"log_to_server",
"disk_mapping",
"production_version",
"staging_version"
}
@abstractmethod
def save_studio_settings(self, data):
"""Save studio overrides of system settings.
Do not use to store whole system settings data with defaults but only
it's overrides with metadata defining how overrides should be applied
in load function. For loading should be used function
`studio_system_settings`.
Args:
data(dict): Data of studio overrides with override metadata.
"""
pass
@abstractmethod
def save_project_settings(self, project_name, overrides):
"""Save studio overrides of project settings.
Data are saved for specific project or as defaults for all projects.
Do not use to store whole project settings data with defaults but only
it's overrides with metadata defining how overrides should be applied
in load function. For loading should be used function
`get_studio_project_settings_overrides` for global project settings
and `get_project_settings_overrides` for project specific settings.
Args:
project_name(str, null): Project name for which overrides are
or None for global settings.
data(dict): Data of project overrides with override metadata.
"""
pass
@abstractmethod
def save_project_anatomy(self, project_name, anatomy_data):
"""Save studio overrides of project anatomy data.
Args:
project_name(str, null): Project name for which overrides are
or None for global settings.
data(dict): Data of project overrides with override metadata.
"""
pass
@abstractmethod
def get_studio_system_settings_overrides(self, return_version):
"""Studio overrides of system settings."""
pass
@abstractmethod
def get_studio_project_settings_overrides(self, return_version):
"""Studio overrides of default project settings."""
pass
@abstractmethod
def get_studio_project_anatomy_overrides(self, return_version):
"""Studio overrides of default project anatomy data."""
pass
@abstractmethod
def get_project_settings_overrides(self, project_name, return_version):
"""Studio overrides of project settings for specific project.
Args:
project_name(str): Name of project for which data should be loaded.
return_version(bool): Version string will be added to output.
Returns:
dict: Only overrides for entered project, may be empty dictionary.
"""
pass
@abstractmethod
def get_project_anatomy_overrides(self, project_name, return_version):
"""Studio overrides of project anatomy for specific project.
Args:
project_name(str): Name of project for which data should be loaded.
return_version(bool): Version string will be added to output.
Returns:
dict: Only overrides for entered project, may be empty dictionary.
"""
pass
# Getters for specific version overrides
@abstractmethod
def get_studio_system_settings_overrides_for_version(self, version):
"""Studio system settings overrides for specific version.
Args:
version(str): OpenPype version for which settings should be
returned.
Returns:
None: If the version does not have system settings overrides.
dict: Document with overrides data.
"""
pass
@abstractmethod
def get_studio_project_anatomy_overrides_for_version(self, version):
"""Studio project anatomy overrides for specific version.
Args:
version(str): OpenPype version for which settings should be
returned.
Returns:
None: If the version does not have system settings overrides.
dict: Document with overrides data.
"""
pass
@abstractmethod
def get_studio_project_settings_overrides_for_version(self, version):
"""Studio project settings overrides for specific version.
Args:
version(str): OpenPype version for which settings should be
returned.
Returns:
None: If the version does not have system settings overrides.
dict: Document with overrides data.
"""
pass
@abstractmethod
def get_project_settings_overrides_for_version(
self, project_name, version
):
"""Studio project settings overrides for specific project and version.
Args:
project_name(str): Name of project for which the overrides should
be loaded.
version(str): OpenPype version for which settings should be
returned.
Returns:
None: If the version does not have system settings overrides.
dict: Document with overrides data.
"""
pass
@abstractmethod
def get_global_settings(self):
"""Studio global settings available across versions.
Output must contain all keys from 'global_keys'. If value is not set
the output value should be 'None'.
Returns:
Dict[str, Any]: Global settings same across versions.
"""
pass
# Clear methods - per version
# - clearing may be helpfull when a version settings were created for
# testing purposes
@abstractmethod
def clear_studio_system_settings_overrides_for_version(self, version):
"""Remove studio system settings overrides for specific version.
If version is not available then skip processing.
"""
pass
@abstractmethod
def clear_studio_project_settings_overrides_for_version(self, version):
"""Remove studio project settings overrides for specific version.
If version is not available then skip processing.
"""
pass
@abstractmethod
def clear_studio_project_anatomy_overrides_for_version(self, version):
"""Remove studio project anatomy overrides for specific version.
If version is not available then skip processing.
"""
pass
@abstractmethod
def clear_project_settings_overrides_for_version(
self, version, project_name
):
"""Remove studio project settings overrides for project and version.
If version is not available then skip processing.
"""
pass
# Get versions that are available for each type of settings
@abstractmethod
def get_available_studio_system_settings_overrides_versions(
self, sorted=None
):
"""OpenPype versions that have any studio system settings overrides.
Returns:
list<str>: OpenPype versions strings.
"""
pass
@abstractmethod
def get_available_studio_project_anatomy_overrides_versions(
self, sorted=None
):
"""OpenPype versions that have any studio project anatomy overrides.
Returns:
List[str]: OpenPype versions strings.
"""
pass
@abstractmethod
def get_available_studio_project_settings_overrides_versions(
self, sorted=None
):
"""OpenPype versions that have any studio project settings overrides.
Returns:
List[str]: OpenPype versions strings.
"""
pass
@abstractmethod
def get_available_project_settings_overrides_versions(
self, project_name, sorted=None
):
"""OpenPype versions that have any project settings overrides.
Args:
project_name(str): Name of project.
Returns:
List[str]: OpenPype versions strings.
"""
pass
@abstractmethod
def get_system_last_saved_info(self):
"""State of last system settings overrides at the moment when called.
This method must provide most recent data so using cached data is not
the way.
Returns:
SettingsStateInfo: Information about system settings overrides.
"""
pass
@abstractmethod
def get_project_last_saved_info(self, project_name):
"""State of last project settings overrides at the moment when called.
This method must provide most recent data so using cached data is not
the way.
Args:
project_name (Union[None, str]): Project name for which state
should be returned.
Returns:
SettingsStateInfo: Information about project settings overrides.
"""
pass
# UI related calls
@abstractmethod
def get_last_opened_info(self):
"""Get information about last opened UI.
Last opened UI is empty if there is noone who would have opened UI at
the moment when called.
Returns:
Union[None, SettingsStateInfo]: Information about machine who had
opened Settings UI.
"""
pass
@abstractmethod
def opened_settings_ui(self):
"""Callback called when settings UI is opened.
Information about this machine must be available when
'get_last_opened_info' is called from anywhere until
'closed_settings_ui' is called again.
Returns:
SettingsStateInfo: Object representing information about this
machine. Must be passed to 'closed_settings_ui' when finished.
"""
pass
@abstractmethod
def closed_settings_ui(self, info_obj):
"""Callback called when settings UI is closed.
From the moment this method is called the information about this
machine is removed and no more available when 'get_last_opened_info'
is called.
Callback should validate if this machine is still stored as opened ui
before changing any value.
Args:
info_obj (SettingsStateInfo): Object created when
'opened_settings_ui' was called.
"""
pass
@six.add_metaclass(ABCMeta)
class LocalSettingsHandler:
"""Handler that should handle about storing and loading of local settings.
Local settings are "workstation" specific modifications that modify how
system and project settings look on the workstation and only there.
"""
@abstractmethod
def save_local_settings(self, data):
"""Save local data of local settings.
Args:
data(dict): Data of local data with override metadata.
"""
pass
@abstractmethod
def get_local_settings(self):
"""Studio overrides of system settings."""
pass
class CacheValues:
cache_lifetime = 10
def __init__(self):
self.data = None
self.creation_time = None
self.version = None
self.last_saved_info = None
def data_copy(self):
if not self.data:
return {}
return copy.deepcopy(self.data)
def update_data(self, data, version):
self.data = data
self.creation_time = datetime.datetime.now()
self.version = version
def update_last_saved_info(self, last_saved_info):
self.last_saved_info = last_saved_info
def update_from_document(self, document, version):
data = {}
if document:
if "data" in document:
data = document["data"]
elif "value" in document:
value = document["value"]
if value:
data = json.loads(value)
self.data = data
self.version = version
def to_json_string(self):
return json.dumps(self.data or {})
@property
def is_outdated(self):
if self.creation_time is None:
return True
delta = (datetime.datetime.now() - self.creation_time).seconds
return delta > self.cache_lifetime
def set_outdated(self):
self.create_time = None
class MongoSettingsHandler(SettingsHandler):
"""Settings handler that use mongo for storing and loading of settings."""
key_suffix = "_versioned"
_version_order_key = "versions_order"
_all_versions_keys = "all_versions"
def __init__(self):
# Get mongo connection
settings_collection = OpenPypeMongoConnection.get_mongo_client()
self._anatomy_keys = None
self._attribute_keys = None
self._version_order_checked = False
self._system_settings_key = SYSTEM_SETTINGS_KEY + self.key_suffix
self._project_settings_key = PROJECT_SETTINGS_KEY + self.key_suffix
self._project_anatomy_key = PROJECT_ANATOMY_KEY + self.key_suffix
self._current_version = openpype.version.__version__
database_name = os.environ["OPENPYPE_DATABASE_NAME"]
# TODO modify to not use hardcoded keys
collection_name = "settings"
self.settings_collection = settings_collection
self.database_name = database_name
self.collection_name = collection_name
self.collection = settings_collection[database_name][collection_name]
self.global_settings_cache = CacheValues()
self.system_settings_cache = CacheValues()
self.project_settings_cache = collections.defaultdict(CacheValues)
self.project_anatomy_cache = collections.defaultdict(CacheValues)
def _prepare_project_settings_keys(self):
from .entities import ProjectSettings
# Prepare anatomy keys and attribute keys
# NOTE this is cached on first import
# - keys may change only on schema change which should not happen
# during production
project_settings_root = ProjectSettings(
reset=False, change_state=False
)
anatomy_entity = project_settings_root["project_anatomy"]
anatomy_keys = set(anatomy_entity.keys())
anatomy_keys.remove("attributes")
attribute_keys = set(anatomy_entity["attributes"].keys())
self._anatomy_keys = anatomy_keys
self._attribute_keys = attribute_keys
@property
def anatomy_keys(self):
if self._anatomy_keys is None:
self._prepare_project_settings_keys()
return self._anatomy_keys
@property
def attribute_keys(self):
if self._attribute_keys is None:
self._prepare_project_settings_keys()
return self._attribute_keys
def get_global_settings_doc(self):
if self.global_settings_cache.is_outdated:
global_settings_doc = self.collection.find_one({
"type": GLOBAL_SETTINGS_KEY
}) or {}
self.global_settings_cache.update_data(global_settings_doc, None)
return self.global_settings_cache.data_copy()
def get_global_settings(self):
global_settings_doc = self.get_global_settings_doc()
global_settings = global_settings_doc.get("data", {})
return {
key: global_settings[key]
for key in self.global_keys
if key in global_settings
}
def _extract_global_settings(self, data):
"""Extract global settings data from system settings overrides.
This is now limited to "general" key in system settings which must be
set as group in schemas.
Returns:
dict: Global settings extracted from system settings data.
"""
output = {}
if "general" not in data:
return output
general_data = data["general"]
# Add predefined keys to global settings if are set
for key in self.global_keys:
if key not in general_data:
continue
# Pop key from values
output[key] = general_data.pop(key)
# Pop key from overridden metadata
if (
M_OVERRIDDEN_KEY in general_data
and key in general_data[M_OVERRIDDEN_KEY]
):
general_data[M_OVERRIDDEN_KEY].remove(key)
return output
def _apply_global_settings(
self, system_settings_document, globals_document
):
"""Apply global settings data to system settings.
Applification is skipped if document with global settings is not
available or does not have set data in.
System settings document is "faked" like it exists if global document
has set values.
Args:
system_settings_document (dict): System settings document from
MongoDB.
globals_document (dict): Global settings document from MongoDB.
Returns:
Merged document which has applied global settings data.
"""
# Skip if globals document is not available
if (
not globals_document
or "data" not in globals_document
or not globals_document["data"]
):
return system_settings_document
globals_data = globals_document["data"]
# Check if data contain any key from predefined keys
any_key_found = False
if globals_data:
for key in self.global_keys:
if key in globals_data:
any_key_found = True
break
# Skip if any key from predefined key was not found in globals
if not any_key_found:
return system_settings_document
# "Fake" system settings document if document does not exist
# - global settings document may exist but system settings not yet
if not system_settings_document:
system_settings_document = {}
if "data" in system_settings_document:
system_settings_data = system_settings_document["data"]
else:
system_settings_data = {}
system_settings_document["data"] = system_settings_data
if "general" in system_settings_data:
system_general = system_settings_data["general"]
else:
system_general = {}
system_settings_data["general"] = system_general
overridden_keys = system_general.get(M_OVERRIDDEN_KEY) or []
for key in self.global_keys:
if key not in globals_data:
continue
system_general[key] = globals_data[key]
if key not in overridden_keys:
overridden_keys.append(key)
if overridden_keys:
system_general[M_OVERRIDDEN_KEY] = overridden_keys
return system_settings_document
def save_studio_settings(self, data):
"""Save studio overrides of system settings.
Do not use to store whole system settings data with defaults but only
it's overrides with metadata defining how overrides should be applied
in load function. For loading should be used function
`studio_system_settings`.
Args:
data(dict): Data of studio overrides with override metadata.
"""
# Update cache
self.system_settings_cache.update_data(data, self._current_version)
last_saved_info = SettingsStateInfo.create_new(
self._current_version,
SYSTEM_SETTINGS_KEY
)
self.system_settings_cache.update_last_saved_info(
last_saved_info
)
# Get copy of just updated cache
system_settings_data = self.system_settings_cache.data_copy()
# Extract global settings from system settings
global_settings = self._extract_global_settings(
system_settings_data
)
self.global_settings_cache.update_data(
global_settings,
None
)
system_settings_doc = self.collection.find_one(
{
"type": self._system_settings_key,
"version": self._current_version
},
{"_id": True}
)
# Store system settings
new_system_settings_doc = {
"type": self._system_settings_key,
"version": self._current_version,
"data": system_settings_data,
"last_saved_info": last_saved_info.to_document_data()
}
if not system_settings_doc:
self.collection.insert_one(new_system_settings_doc)
else:
self.collection.update_one(
{"_id": system_settings_doc["_id"]},
{"$set": new_system_settings_doc}
)
# Store global settings
self.collection.replace_one(
{
"type": GLOBAL_SETTINGS_KEY
},
{
"type": GLOBAL_SETTINGS_KEY,
"data": global_settings
},
upsert=True
)
def save_project_settings(self, project_name, overrides):
"""Save studio overrides of project settings.
Data are saved for specific project or as defaults for all projects.
Do not use to store whole project settings data with defaults but only
it's overrides with metadata defining how overrides should be applied
in load function. For loading should be used function
`get_studio_project_settings_overrides` for global project settings
and `get_project_settings_overrides` for project specific settings.
Args:
project_name(str, null): Project name for which overrides are
or None for global settings.
data(dict): Data of project overrides with override metadata.
"""
data_cache = self.project_settings_cache[project_name]
data_cache.update_data(overrides, self._current_version)
last_saved_info = SettingsStateInfo.create_new(
self._current_version,
PROJECT_SETTINGS_KEY,
project_name
)
data_cache.update_last_saved_info(last_saved_info)
self._save_project_data(
project_name,
self._project_settings_key,
data_cache,
last_saved_info
)
def save_project_anatomy(self, project_name, anatomy_data):
"""Save studio overrides of project anatomy data.
Args:
project_name(str, null): Project name for which overrides are
or None for global settings.
data(dict): Data of project overrides with override metadata.
"""
data_cache = self.project_anatomy_cache[project_name]
data_cache.update_data(anatomy_data, self._current_version)
if project_name is not None:
self._save_project_anatomy_data(project_name, data_cache)
else:
last_saved_info = SettingsStateInfo.create_new(
self._current_version,
PROJECT_ANATOMY_KEY,
project_name
)
self._save_project_data(
project_name,
self._project_anatomy_key,
data_cache,
last_saved_info
)
@classmethod
def prepare_mongo_update_dict(cls, in_data):
data = {}
for key, value in in_data.items():
if not isinstance(value, dict):
data[key] = value
continue
new_value = cls.prepare_mongo_update_dict(value)
for _key, _value in new_value.items():
new_key = ".".join((key, _key))
data[new_key] = _value
return data
def _save_project_anatomy_data(self, project_name, data_cache):
# Create copy of data as they will be modified during save
new_data = data_cache.data_copy()
# Prepare avalon project document
project_doc = get_project(project_name)
if not project_doc:
raise ValueError((
"Project document of project \"{}\" does not exists."
" Create project first."
).format(project_name))
collection = get_project_connection(project_name)
# Project's data
update_dict_data = {}
project_doc_data = project_doc.get("data") or {}
attributes = new_data.pop("attributes")
_applications = attributes.pop("applications", None) or []
for key, value in attributes.items():
if (
key in project_doc_data
and project_doc_data[key] == value
):
continue
update_dict_data[key] = value
update_dict_config = {}
applications = []
for application in _applications:
if not application:
continue
if isinstance(application, six.string_types):
applications.append({"name": application})
new_data["apps"] = applications
for key, value in new_data.items():
project_doc_value = project_doc.get(key)
if key in project_doc and project_doc_value == value:
continue
update_dict_config[key] = value
if not update_dict_data and not update_dict_config:
return
data_changes = self.prepare_mongo_update_dict(update_dict_data)
# Update dictionary of changes that will be changed in mongo
update_dict = {}
for key, value in data_changes.items():
new_key = "data.{}".format(key)
update_dict[new_key] = value
for key, value in update_dict_config.items():
new_key = "config.{}".format(key)
update_dict[new_key] = value
collection.update_one(
{"type": "project"},
{"$set": update_dict}
)
def _save_project_data(
self, project_name, doc_type, data_cache, last_saved_info
):
is_default = bool(project_name is None)
query_filter = {
"type": doc_type,
"is_default": is_default,
"version": self._current_version
}
new_project_settings_doc = {
"type": doc_type,
"data": data_cache.data,
"is_default": is_default,
"version": self._current_version,
"last_saved_info": last_saved_info.to_data()
}
if not is_default:
query_filter["project_name"] = project_name
new_project_settings_doc["project_name"] = project_name
project_settings_doc = self.collection.find_one(
query_filter,
{"_id": True}
)
if project_settings_doc:
self.collection.update_one(
{"_id": project_settings_doc["_id"]},
{"$set": new_project_settings_doc}
)
else:
self.collection.insert_one(new_project_settings_doc)
def _get_versions_order_doc(self, projection=None):
# TODO cache
return self.collection.find_one(
{"type": self._version_order_key},
projection
) or {}
def _check_version_order(self):
"""This method will work only in OpenPype process.
Will create/update mongo document where OpenPype versions are stored
in semantic version order.
This document can be then used to find closes version of settings in
processes where 'OpenPypeVersion' is not available.
"""
# Do this step only once
if self._version_order_checked:
return
self._version_order_checked = True
from openpype.lib.openpype_version import get_OpenPypeVersion
OpenPypeVersion = get_OpenPypeVersion()
# Skip if 'OpenPypeVersion' is not available
if OpenPypeVersion is None:
return
# Query document holding sorted list of version strings
doc = self._get_versions_order_doc()
if not doc:
doc = {"type": self._version_order_key}
if self._all_versions_keys not in doc:
doc[self._all_versions_keys] = []
# Skip if current version is already available
if self._current_version in doc[self._all_versions_keys]:
return
if self._current_version not in doc[self._all_versions_keys]:
# Add all versions into list
all_objected_versions = [
OpenPypeVersion(version=self._current_version)
]
for version_str in doc[self._all_versions_keys]:
all_objected_versions.append(
OpenPypeVersion(version=version_str)
)
doc[self._all_versions_keys] = [
str(version) for version in sorted(all_objected_versions)
]
self.collection.replace_one(
{"type": self._version_order_key},
doc,
upsert=True
)
def find_closest_version_for_projects(self, project_names):
output = {
project_name: None
for project_name in project_names
}
from openpype.lib.openpype_version import get_OpenPypeVersion
OpenPypeVersion = get_OpenPypeVersion()
if OpenPypeVersion is None:
return output
versioned_doc = self._get_versions_order_doc()
settings_ids = []
for project_name in project_names:
if project_name is None:
doc_filter = {"is_default": True}
else:
doc_filter = {"project_name": project_name}
settings_id = self._find_closest_settings_id(
self._project_settings_key,
PROJECT_SETTINGS_KEY,
doc_filter,
versioned_doc
)
if settings_id:
settings_ids.append(settings_id)
if settings_ids:
docs = self.collection.find(
{"_id": {"$in": settings_ids}},
{"version": True, "project_name": True}
)
for doc in docs:
project_name = doc.get("project_name")
version = doc.get("version", LEGACY_SETTINGS_VERSION)
output[project_name] = version
return output
def _find_closest_settings_id(
self, key, legacy_key, additional_filters=None, versioned_doc=None
):
"""Try to find closes available versioned settings for settings key.
This method should be used only if settings for current OpenPype
version are not available.
Args:
key(str): Settings key under which are settings stored ("type").
legacy_key(str): Settings key under which were stored not versioned
settings.
additional_filters(dict): Additional filters of document. Used
for project specific settings.
"""
# Trigger check of versions
self._check_version_order()
doc_filters = {
"type": {"$in": [key, legacy_key]}
}
if additional_filters:
doc_filters.update(additional_filters)
# Query base data of each settings doc
other_versions = self.collection.find(
doc_filters,
{
"_id": True,
"version": True,
"type": True
}
)
# Query doc with list of sorted versions
if versioned_doc is None:
versioned_doc = self._get_versions_order_doc()
# Separate queried docs
legacy_settings_doc = None
versioned_settings_by_version = {}
for doc in other_versions:
if doc["type"] == legacy_key:
legacy_settings_doc = doc
elif doc["type"] == key:
if doc["version"] == self._current_version:
return doc["_id"]
versioned_settings_by_version[doc["version"]] = doc
versions_in_doc = versioned_doc.get(self._all_versions_keys) or []
# Cases when only legacy settings can be used
if (
# There are not versioned documents yet
not versioned_settings_by_version
# Versioned document is not available at all
# - this can happen only if old build of OpenPype was used
or not versioned_doc
# Current OpenPype version is not available
# - something went really wrong when this happens
or self._current_version not in versions_in_doc
):
if not legacy_settings_doc:
return None
return legacy_settings_doc["_id"]
# Separate versions to lower and higher and keep their order
lower_versions = []
higher_versions = []
before = True
for version_str in versions_in_doc:
if version_str == self._current_version:
before = False
elif before:
lower_versions.append(version_str)
else:
higher_versions.append(version_str)
# Use legacy settings doc as source document
src_doc_id = None
if legacy_settings_doc:
src_doc_id = legacy_settings_doc["_id"]
# Find highest version which has available settings
if lower_versions:
for version_str in reversed(lower_versions):
doc = versioned_settings_by_version.get(version_str)
if doc:
src_doc_id = doc["_id"]
break
# Use versions with higher version only if there are not legacy
# settings and there are not any versions before
if src_doc_id is None and higher_versions:
for version_str in higher_versions:
doc = versioned_settings_by_version.get(version_str)
if doc:
src_doc_id = doc["_id"]
break
return src_doc_id
def _find_closest_settings(
self, key, legacy_key, additional_filters=None, versioned_doc=None
):
doc_id = self._find_closest_settings_id(
key, legacy_key, additional_filters, versioned_doc
)
if doc_id is None:
return None
return self.collection.find_one({"_id": doc_id})
def _find_closest_system_settings(self):
return self._find_closest_settings(
self._system_settings_key,
SYSTEM_SETTINGS_KEY
)
def _find_closest_project_settings(self, project_name):
if project_name is None:
additional_filters = {"is_default": True}
else:
additional_filters = {"project_name": project_name}
return self._find_closest_settings(
self._project_settings_key,
PROJECT_SETTINGS_KEY,
additional_filters
)
def _find_closest_project_anatomy(self):
additional_filters = {"is_default": True}
return self._find_closest_settings(
self._project_anatomy_key,
PROJECT_ANATOMY_KEY,
additional_filters
)
def _get_studio_system_settings_overrides_for_version(self, version=None):
# QUESTION cache?
if version == LEGACY_SETTINGS_VERSION:
return self.collection.find_one({
"type": SYSTEM_SETTINGS_KEY
})
if version is None:
version = self._current_version
return self.collection.find_one({
"type": self._system_settings_key,
"version": version
})
def _get_project_settings_overrides_for_version(
self, project_name, version=None
):
# QUESTION cache?
if version == LEGACY_SETTINGS_VERSION:
document_filter = {
"type": PROJECT_SETTINGS_KEY
}
else:
if version is None:
version = self._current_version
document_filter = {
"type": self._project_settings_key,
"version": version
}
if project_name is None:
document_filter["is_default"] = True
else:
document_filter["project_name"] = project_name
return self.collection.find_one(document_filter)
def _get_project_anatomy_overrides_for_version(self, version=None):
# QUESTION cache?
if version == LEGACY_SETTINGS_VERSION:
return self.collection.find_one({
"type": PROJECT_ANATOMY_KEY,
"is_default": True
})
if version is None:
version = self._current_version
return self.collection.find_one({
"type": self._project_anatomy_key,
"is_default": True,
"version": version
})
def get_studio_system_settings_overrides(self, return_version):
"""Studio overrides of system settings."""
if self.system_settings_cache.is_outdated:
globals_document = self.get_global_settings_doc()
document, version = self._get_system_settings_overrides_doc()
last_saved_info = SettingsStateInfo.from_document(
version, SYSTEM_SETTINGS_KEY, document
)
merged_document = self._apply_global_settings(
document, globals_document
)
self.system_settings_cache.update_from_document(
merged_document, version
)
self.system_settings_cache.update_last_saved_info(
last_saved_info
)
cache = self.system_settings_cache
data = cache.data_copy()
if return_version:
return data, cache.version
return data
def _get_system_settings_overrides_doc(self):
document = (
self._get_studio_system_settings_overrides_for_version()
)
if document is None:
document = self._find_closest_system_settings()
version = None
if document:
if document["type"] == self._system_settings_key:
version = document["version"]
else:
version = LEGACY_SETTINGS_VERSION
return document, version
def get_system_last_saved_info(self):
# Make sure settings are recaches
self.system_settings_cache.set_outdated()
self.get_studio_system_settings_overrides(False)
return self.system_settings_cache.last_saved_info.copy()
def _get_project_settings_overrides(self, project_name, return_version):
if self.project_settings_cache[project_name].is_outdated:
document, version = self._get_project_settings_overrides_doc(
project_name
)
self.project_settings_cache[project_name].update_from_document(
document, version
)
last_saved_info = SettingsStateInfo.from_document(
version, PROJECT_SETTINGS_KEY, document
)
self.project_settings_cache[project_name].update_last_saved_info(
last_saved_info
)
cache = self.project_settings_cache[project_name]
data = cache.data_copy()
if return_version:
return data, cache.version
return data
def _get_project_settings_overrides_doc(self, project_name):
document = self._get_project_settings_overrides_for_version(
project_name
)
if document is None:
document = self._find_closest_project_settings(project_name)
version = None
if document:
if document["type"] == self._project_settings_key:
version = document["version"]
else:
version = LEGACY_SETTINGS_VERSION
return document, version
def get_project_last_saved_info(self, project_name):
# Make sure settings are recaches
self.project_settings_cache[project_name].set_outdated()
self._get_project_settings_overrides(project_name, False)
return self.project_settings_cache[project_name].last_saved_info.copy()
def get_studio_project_settings_overrides(self, return_version):
"""Studio overrides of default project settings."""
return self._get_project_settings_overrides(None, return_version)
def get_project_settings_overrides(self, project_name, return_version):
"""Studio overrides of project settings for specific project.
Args:
project_name(str): Name of project for which data should be loaded.
Returns:
dict: Only overrides for entered project, may be empty dictionary.
"""
if not project_name:
if return_version:
return {}, None
return {}
return self._get_project_settings_overrides(
project_name, return_version
)
def project_doc_to_anatomy_data(self, project_doc):
"""Convert project document to anatomy data.
Probably should fill missing keys and values.
"""
if not project_doc:
return {}
attributes = {}
project_doc_data = project_doc.get("data") or {}
for key in self.attribute_keys:
value = project_doc_data.get(key)
if value is not None:
attributes[key] = value
project_doc_config = project_doc.get("config") or {}
app_names = set()
if not project_doc_config or "apps" not in project_doc_config:
set_applications = False
else:
set_applications = True
for app_item in project_doc_config["apps"]:
if not app_item:
continue
app_name = app_item.get("name")
if app_name:
app_names.add(app_name)
if set_applications:
attributes["applications"] = list(app_names)
output = {"attributes": attributes}
for key in self.anatomy_keys:
value = project_doc_config.get(key)
if value is not None:
output[key] = value
return output
def _get_project_anatomy_overrides(self, project_name, return_version):
if self.project_anatomy_cache[project_name].is_outdated:
if project_name is None:
document = self._get_project_anatomy_overrides_for_version()
if document is None:
document = self._find_closest_project_anatomy()
version = None
if document:
if document["type"] == self._project_anatomy_key:
version = document["version"]
else:
version = LEGACY_SETTINGS_VERSION
self.project_anatomy_cache[project_name].update_from_document(
document, version
)
else:
project_doc = get_project(project_name)
self.project_anatomy_cache[project_name].update_data(
self.project_doc_to_anatomy_data(project_doc),
self._current_version
)
cache = self.project_anatomy_cache[project_name]
data = cache.data_copy()
if return_version:
return data, cache.version
return data
def get_studio_project_anatomy_overrides(self, return_version):
"""Studio overrides of default project anatomy data."""
return self._get_project_anatomy_overrides(None, return_version)
def get_project_anatomy_overrides(self, project_name):
"""Studio overrides of project anatomy for specific project.
Args:
project_name(str): Name of project for which data should be loaded.
Returns:
dict: Only overrides for entered project, may be empty dictionary.
"""
if not project_name:
return {}
return self._get_project_anatomy_overrides(project_name, False)
# Implementations of abstract methods to get overrides for version
def get_studio_system_settings_overrides_for_version(self, version):
doc = self._get_studio_system_settings_overrides_for_version(version)
if not doc:
return doc
return doc["data"]
def get_studio_project_anatomy_overrides_for_version(self, version):
doc = self._get_project_anatomy_overrides_for_version(version)
if not doc:
return doc
return doc["data"]
def get_studio_project_settings_overrides_for_version(self, version):
doc = self._get_project_settings_overrides_for_version(None, version)
if not doc:
return doc
return doc["data"]
def get_project_settings_overrides_for_version(
self, project_name, version
):
doc = self._get_project_settings_overrides_for_version(
project_name, version
)
if not doc:
return doc
return doc["data"]
# Implementations of abstract methods to clear overrides for version
def clear_studio_system_settings_overrides_for_version(self, version):
self.collection.delete_one({
"type": self._system_settings_key,
"version": version
})
def clear_studio_project_settings_overrides_for_version(self, version):
self.collection.delete_one({
"type": self._project_settings_key,
"version": version,
"is_default": True
})
def clear_studio_project_anatomy_overrides_for_version(self, version):
self.collection.delete_one({
"type": self._project_anatomy_key,
"version": version
})
def clear_project_settings_overrides_for_version(
self, version, project_name
):
self.collection.delete_one({
"type": self._project_settings_key,
"version": version,
"project_name": project_name
})
def _sort_versions(self, versions):
"""Sort versions.
WARNING:
This method does not handle all possible issues so it should not be
used in logic which determine which settings are used. Is used for
sorting of available versions.
"""
if not versions:
return []
set_versions = set(versions)
contain_legacy = LEGACY_SETTINGS_VERSION in set_versions
if contain_legacy:
set_versions.remove(LEGACY_SETTINGS_VERSION)
from openpype.lib.openpype_version import get_OpenPypeVersion
OpenPypeVersion = get_OpenPypeVersion()
# Skip if 'OpenPypeVersion' is not available
if OpenPypeVersion is not None:
obj_versions = sorted(
[OpenPypeVersion(version=version) for version in set_versions]
)
sorted_versions = [str(version) for version in obj_versions]
if contain_legacy:
sorted_versions.insert(0, LEGACY_SETTINGS_VERSION)
return sorted_versions
doc = self._get_versions_order_doc()
all_versions = doc.get(self._all_versions_keys)
if not all_versions:
return list(sorted(versions))
sorted_versions = []
for version in all_versions:
if version in set_versions:
set_versions.remove(version)
sorted_versions.append(version)
for version in sorted(set_versions):
sorted_versions.insert(0, version)
if contain_legacy:
sorted_versions.insert(0, LEGACY_SETTINGS_VERSION)
return sorted_versions
# Get available versions for settings type
def get_available_studio_system_settings_overrides_versions(
self, sorted=None
):
docs = self.collection.find(
{"type": {
"$in": [self._system_settings_key, SYSTEM_SETTINGS_KEY]
}},
{"type": True, "version": True}
)
output = set()
for doc in docs:
if doc["type"] == self._system_settings_key:
output.add(doc["version"])
else:
output.add(LEGACY_SETTINGS_VERSION)
if not sorted:
return output
return self._sort_versions(output)
def get_available_studio_project_anatomy_overrides_versions(
self, sorted=None
):
docs = self.collection.find(
{"type": {
"$in": [self._project_anatomy_key, PROJECT_ANATOMY_KEY]
}},
{"type": True, "version": True}
)
output = set()
for doc in docs:
if doc["type"] == self._project_anatomy_key:
output.add(doc["version"])
else:
output.add(LEGACY_SETTINGS_VERSION)
if not sorted:
return output
return self._sort_versions(output)
def get_available_studio_project_settings_overrides_versions(
self, sorted=None
):
docs = self.collection.find(
{
"is_default": True,
"type": {
"$in": [self._project_settings_key, PROJECT_SETTINGS_KEY]
}
},
{"type": True, "version": True}
)
output = set()
for doc in docs:
if doc["type"] == self._project_settings_key:
output.add(doc["version"])
else:
output.add(LEGACY_SETTINGS_VERSION)
if not sorted:
return output
return self._sort_versions(output)
def get_available_project_settings_overrides_versions(
self, project_name, sorted=None
):
docs = self.collection.find(
{
"project_name": project_name,
"type": {
"$in": [self._project_settings_key, PROJECT_SETTINGS_KEY]
}
},
{"type": True, "version": True}
)
output = set()
for doc in docs:
if doc["type"] == self._project_settings_key:
output.add(doc["version"])
else:
output.add(LEGACY_SETTINGS_VERSION)
if not sorted:
return output
return self._sort_versions(output)
def get_last_opened_info(self):
doc = self.collection.find_one({
"type": "last_opened_settings_ui",
"version": self._current_version
}) or {}
info_data = doc.get("info")
if not info_data:
return None
# Fill not available information
info_data["openpype_version"] = self._current_version
info_data["settings_type"] = None
info_data["project_name"] = None
return SettingsStateInfo.from_data(info_data)
def opened_settings_ui(self):
doc_filter = {
"type": "last_opened_settings_ui",
"version": self._current_version
}
opened_info = SettingsStateInfo.create_new(self._current_version)
new_doc_data = copy.deepcopy(doc_filter)
new_doc_data["info"] = opened_info.to_document_data()
doc = self.collection.find_one(
doc_filter,
{"_id": True}
)
if doc:
self.collection.update_one(
{"_id": doc["_id"]},
{"$set": new_doc_data}
)
else:
self.collection.insert_one(new_doc_data)
return opened_info
def closed_settings_ui(self, info_obj):
doc_filter = {
"type": "last_opened_settings_ui",
"version": self._current_version
}
doc = self.collection.find_one(doc_filter) or {}
info_data = doc.get("info")
if not info_data:
return
info_data["openpype_version"] = self._current_version
info_data["settings_type"] = None
info_data["project_name"] = None
current_info = SettingsStateInfo.from_data(info_data)
if current_info == info_obj:
self.collection.update_one(
{"_id": doc["_id"]},
{"$set": {"info": None}}
)
class MongoLocalSettingsHandler(LocalSettingsHandler):
"""Settings handler that use mongo for store and load local settings.
Data have 2 query criteria. First is key "type" stored in constant
`LOCAL_SETTING_KEY`. Second is key "site_id" which value can be obstained
with `get_local_site_id` function.
"""
def __init__(self, local_site_id=None):
# Get mongo connection
from openpype.lib import (
OpenPypeMongoConnection,
get_local_site_id
)
if local_site_id is None:
local_site_id = get_local_site_id()
settings_collection = OpenPypeMongoConnection.get_mongo_client()
# TODO prepare version of pype
# - pype version should define how are settings saved and loaded
database_name = os.environ["OPENPYPE_DATABASE_NAME"]
# TODO modify to not use hardcoded keys
collection_name = "settings"
self.settings_collection = settings_collection
self.database_name = database_name
self.collection_name = collection_name
self.collection = settings_collection[database_name][collection_name]
self.local_site_id = local_site_id
self.local_settings_cache = CacheValues()
def save_local_settings(self, data):
"""Save local settings.
Args:
data(dict): Data of studio overrides with override metadata.
"""
data = data or {}
self.local_settings_cache.update_data(data, None)
self.collection.replace_one(
{
"type": LOCAL_SETTING_KEY,
"site_id": self.local_site_id
},
{
"type": LOCAL_SETTING_KEY,
"site_id": self.local_site_id,
"data": self.local_settings_cache.data
},
upsert=True
)
def get_local_settings(self):
"""Local settings for local site id."""
if self.local_settings_cache.is_outdated:
document = self.collection.find_one({
"type": LOCAL_SETTING_KEY,
"site_id": self.local_site_id
})
self.local_settings_cache.update_from_document(document, None)
return self.local_settings_cache.data_copy()