mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-26 05:42:15 +01:00
603 lines
20 KiB
Python
603 lines
20 KiB
Python
import os
|
|
import json
|
|
import copy
|
|
import logging
|
|
import collections
|
|
import datetime
|
|
from abc import ABCMeta, abstractmethod
|
|
import six
|
|
import openpype
|
|
from .constants import (
|
|
GLOBAL_SETTINGS_KEY,
|
|
SYSTEM_SETTINGS_KEY,
|
|
PROJECT_SETTINGS_KEY,
|
|
PROJECT_ANATOMY_KEY,
|
|
LOCAL_SETTING_KEY
|
|
)
|
|
from .lib import load_json_file
|
|
|
|
JSON_EXC = getattr(json.decoder, "JSONDecodeError", ValueError)
|
|
|
|
|
|
@six.add_metaclass(ABCMeta)
|
|
class SettingsHandler:
|
|
@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):
|
|
"""Studio overrides of system settings."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_studio_project_settings_overrides(self):
|
|
"""Studio overrides of default project settings."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_studio_project_anatomy_overrides(self):
|
|
"""Studio overrides of default project anatomy data."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_project_settings_overrides(self, project_name):
|
|
"""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.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
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.
|
|
"""
|
|
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
|
|
|
|
def data_copy(self):
|
|
if not self.data:
|
|
return {}
|
|
return copy.deepcopy(self.data)
|
|
|
|
def update_data(self, data):
|
|
self.data = data
|
|
self.creation_time = datetime.datetime.now()
|
|
|
|
def update_from_document(self, document):
|
|
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
|
|
|
|
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
|
|
|
|
|
|
class MongoSettingsHandler(SettingsHandler):
|
|
"""Settings handler that use mongo for storing and loading of settings."""
|
|
|
|
def __init__(self):
|
|
# Get mongo connection
|
|
from openpype.lib import OpenPypeMongoConnection
|
|
from avalon.api import AvalonMongoDB
|
|
|
|
settings_collection = OpenPypeMongoConnection.get_mongo_client()
|
|
|
|
self._anatomy_keys = None
|
|
self._attribute_keys = None
|
|
# 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.avalon_db = AvalonMongoDB()
|
|
|
|
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 _prepare_global_settings(self, data):
|
|
output = {}
|
|
# Add "openpype_path" key to global settings if is set
|
|
if "general" in data and "openpype_path" in data["general"]:
|
|
output["openpype_path"] = data["general"]["openpype_path"]
|
|
return output
|
|
|
|
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.
|
|
"""
|
|
# Store system settings
|
|
self.system_settings_cache.update_data(data)
|
|
self.collection.replace_one(
|
|
{
|
|
"type": SYSTEM_SETTINGS_KEY
|
|
},
|
|
{
|
|
"type": SYSTEM_SETTINGS_KEY,
|
|
"data": self.system_settings_cache.data
|
|
},
|
|
upsert=True
|
|
)
|
|
|
|
# Get global settings from system settings
|
|
global_settings = self._prepare_global_settings(
|
|
self.system_settings_cache.data
|
|
)
|
|
# 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._save_project_data(
|
|
project_name, PROJECT_SETTINGS_KEY, data_cache
|
|
)
|
|
|
|
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)
|
|
|
|
if project_name is not None:
|
|
self._save_project_anatomy_data(project_name, data_cache)
|
|
|
|
else:
|
|
self._save_project_data(
|
|
project_name, PROJECT_ANATOMY_KEY, data_cache
|
|
)
|
|
|
|
@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
|
|
collection = self.avalon_db.database[project_name]
|
|
project_doc = collection.find_one({
|
|
"type": "project"
|
|
})
|
|
if not project_doc:
|
|
raise ValueError((
|
|
"Project document of project \"{}\" does not exists."
|
|
" Create project first."
|
|
).format(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):
|
|
is_default = bool(project_name is None)
|
|
replace_filter = {
|
|
"type": doc_type,
|
|
"is_default": is_default
|
|
}
|
|
replace_data = {
|
|
"type": doc_type,
|
|
"data": data_cache.data,
|
|
"is_default": is_default
|
|
}
|
|
if not is_default:
|
|
replace_filter["project_name"] = project_name
|
|
replace_data["project_name"] = project_name
|
|
|
|
self.collection.replace_one(
|
|
replace_filter,
|
|
replace_data,
|
|
upsert=True
|
|
)
|
|
|
|
def get_studio_system_settings_overrides(self):
|
|
"""Studio overrides of system settings."""
|
|
if self.system_settings_cache.is_outdated:
|
|
document = self.collection.find_one({
|
|
"type": SYSTEM_SETTINGS_KEY
|
|
})
|
|
|
|
self.system_settings_cache.update_from_document(document)
|
|
return self.system_settings_cache.data_copy()
|
|
|
|
def _get_project_settings_overrides(self, project_name):
|
|
if self.project_settings_cache[project_name].is_outdated:
|
|
document_filter = {
|
|
"type": PROJECT_SETTINGS_KEY,
|
|
}
|
|
if project_name is None:
|
|
document_filter["is_default"] = True
|
|
else:
|
|
document_filter["project_name"] = project_name
|
|
document = self.collection.find_one(document_filter)
|
|
self.project_settings_cache[project_name].update_from_document(
|
|
document
|
|
)
|
|
return self.project_settings_cache[project_name].data_copy()
|
|
|
|
def get_studio_project_settings_overrides(self):
|
|
"""Studio overrides of default project settings."""
|
|
return self._get_project_settings_overrides(None)
|
|
|
|
def get_project_settings_overrides(self, project_name):
|
|
"""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:
|
|
return {}
|
|
return self._get_project_settings_overrides(project_name)
|
|
|
|
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 "apps" in project_doc_config:
|
|
for app_item in project_doc_config.pop("apps"):
|
|
if not app_item:
|
|
continue
|
|
app_name = app_item.get("name")
|
|
if app_name:
|
|
app_names.add(app_name)
|
|
|
|
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):
|
|
if self.project_anatomy_cache[project_name].is_outdated:
|
|
if project_name is None:
|
|
document_filter = {
|
|
"type": PROJECT_ANATOMY_KEY,
|
|
"is_default": True
|
|
}
|
|
document = self.collection.find_one(document_filter)
|
|
self.project_anatomy_cache[project_name].update_from_document(
|
|
document
|
|
)
|
|
else:
|
|
collection = self.avalon_db.database[project_name]
|
|
project_doc = collection.find_one({"type": "project"})
|
|
self.project_anatomy_cache[project_name].update_data(
|
|
self.project_doc_to_anatomy_data(project_doc)
|
|
)
|
|
|
|
return self.project_anatomy_cache[project_name].data_copy()
|
|
|
|
def get_studio_project_anatomy_overrides(self):
|
|
"""Studio overrides of default project anatomy data."""
|
|
return self._get_project_anatomy_overrides(None)
|
|
|
|
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)
|
|
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
return self.local_settings_cache.data_copy()
|