diff --git a/pype.py b/pype.py index 21cc97b097..992e0c35ba 100644 --- a/pype.py +++ b/pype.py @@ -48,8 +48,11 @@ from igniter.tools import load_environments, add_acre_to_sys_path from igniter import BootstrapRepos -add_acre_to_sys_path() -import acre +try: + import acre +except ImportError: + add_acre_to_sys_path() + import acre def set_environments() -> None: @@ -101,20 +104,10 @@ def set_modules_environments(): def boot(): """Bootstrap Pype.""" - art = r""" - ____________ - /\ ___ \ - \ \ \/_\ \ - \ \ _____/ ___ ___ ___ - \ \ \___/ ---- \ \\ \\ \ - \ \____\ / \____\ \__\\__\\__\ - \/____/ \/____/ . PYPE Club . - - """ from pype.lib.terminal_splash import play_animation play_animation() - set_environments() + # find pype versions bootstrap = BootstrapRepos() pype_versions = bootstrap.find_pype() @@ -140,6 +133,7 @@ def boot(): else: os.environ["PYPE_MONGO"] = pype_mongo + set_environments() if getattr(sys, 'frozen', False): if not pype_versions: import igniter diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 02b5178311..9444ef5195 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -7,7 +7,8 @@ from .log import PypeLogger, timeit from .mongo import ( decompose_url, compose_url, - get_default_components + get_default_components, + PypeMongoConnection ) from .anatomy import Anatomy @@ -130,6 +131,8 @@ __all__ = [ "decompose_url", "compose_url", "get_default_components", + "PypeMongoConnection", + "IniSettingRegistry", "JSONSettingRegistry", "PypeSettingsRegistry", diff --git a/pype/lib/mongo.py b/pype/lib/mongo.py index f950572c6e..f82c8b5e23 100644 --- a/pype/lib/mongo.py +++ b/pype/lib/mongo.py @@ -1,9 +1,13 @@ import os +import sys +import time +import logging +import pymongo -try: - from urllib.parse import urlparse, parse_qs -except ImportError: +if sys.version_info[0] == 2: from urlparse import urlparse, parse_qs +else: + from urllib.parse import urlparse, parse_qs class MongoEnvNotSet(Exception): @@ -79,3 +83,79 @@ def get_default_components(): "URL for Mongo logging connection is not set." ) return decompose_url(mongo_url) + + +def extract_port_from_url(url): + parsed_url = urlparse(url) + if parsed_url.scheme is None: + _url = "mongodb://{}".format(url) + parsed_url = urlparse(_url) + return parsed_url.port + + +class PypeMongoConnection: + """Singleton MongoDB connection. + + Keeps MongoDB connections by url. + """ + mongo_clients = {} + log = logging.getLogger("PypeMongoConnection") + + @classmethod + def get_mongo_client(cls, mongo_url=None): + if mongo_url is None: + mongo_url = os.environ["PYPE_MONGO"] + + connection = cls.mongo_clients.get(mongo_url) + if connection: + # Naive validation of existing connection + try: + connection.server_info() + except Exception: + connection = None + + if not connection: + cls.log.debug("Creating mongo connection to {}".format(mongo_url)) + connection = cls.create_connection(mongo_url) + cls.mongo_clients[mongo_url] = connection + + return connection + + @classmethod + def create_connection(cls, mongo_url, timeout=None): + if timeout is None: + timeout = int(os.environ.get("AVALON_TIMEOUT") or 1000) + + kwargs = { + "host": mongo_url, + "serverSelectionTimeoutMS": timeout + } + + port = extract_port_from_url(mongo_url) + if port is not None: + kwargs["port"] = int(port) + + mongo_client = pymongo.MongoClient(**kwargs) + + for _retry in range(3): + try: + t1 = time.time() + mongo_client.server_info() + + except Exception: + cls.log.warning("Retrying...") + time.sleep(1) + timeout *= 1.5 + + else: + break + + else: + raise IOError(( + "ERROR: Couldn't connect to {} in less than {:.3f}ms" + ).format(mongo_url, timeout)) + + cls.log.info("Connected to {}, delay {:.3f}s".format( + mongo_url, time.time() - t1 + )) + return mongo_client diff --git a/pype/lib/terminal_splash.py b/pype/lib/terminal_splash.py index 1bb1c488d0..b90dd2891e 100644 --- a/pype/lib/terminal_splash.py +++ b/pype/lib/terminal_splash.py @@ -29,6 +29,6 @@ def play_animation(): # term.aquamarine3_bold(frame) print(f"{term.bold}{term.aquamarine3}{frame}{term.normal}") - sleep(0.05) + sleep(0.035) current_frame += frame_size print(term.move_y(7)) diff --git a/pype/modules/__init__.py b/pype/modules/__init__.py index 11157f24b1..00303aafc6 100644 --- a/pype/modules/__init__.py +++ b/pype/modules/__init__.py @@ -7,7 +7,7 @@ from .base import ( ModulesManager, TrayModulesManager ) - +from .settings_module import SettingsModule from .rest_api import ( RestApiModule, IRestApi @@ -44,6 +44,8 @@ __all__ = ( "ModulesManager", "TrayModulesManager", + "SettingsModule", + "UserModule", "IUserModule", diff --git a/pype/modules/avalon_apps/avalon_app.py b/pype/modules/avalon_apps/avalon_app.py index d80c0afe6f..683d804412 100644 --- a/pype/modules/avalon_apps/avalon_app.py +++ b/pype/modules/avalon_apps/avalon_app.py @@ -9,7 +9,7 @@ from .. import ( class AvalonModule(PypeModule, ITrayModule, IRestApi): - name = "Avalon" + name = "avalon" def initialize(self, modules_settings): # This module is always enabled diff --git a/pype/modules/base.py b/pype/modules/base.py index 72d0eb4503..3c2c2e7e21 100644 --- a/pype/modules/base.py +++ b/pype/modules/base.py @@ -150,15 +150,15 @@ class ITrayService(ITrayModule): if ITrayService._services_submenu is None: from Qt import QtWidgets services_submenu = QtWidgets.QMenu("Services", tray_menu) - services_submenu.setVisible(False) + services_submenu.menuAction().setVisible(False) ITrayService._services_submenu = services_submenu return ITrayService._services_submenu @staticmethod def add_service_action(action): ITrayService._services_submenu.addAction(action) - if not ITrayService._services_submenu.isVisible(): - ITrayService._services_submenu.setVisible(True) + if not ITrayService._services_submenu.menuAction().isVisible(): + ITrayService._services_submenu.menuAction().setVisible(True) @staticmethod def _load_service_icons(): @@ -384,13 +384,14 @@ class ModulesManager: class TrayModulesManager(ModulesManager): # Define order of modules in menu modules_menu_order = ( - "User setting", - "Ftrack", + "user", + "ftrack", "muster", - "Avalon", - "Clockify", - "Standalone Publish", - "Logging" + "avalon", + "clockify", + "standalonepublish_tool", + "log_viewer", + "settings" ) def __init__(self): diff --git a/pype/modules/clockify/clockify_module.py b/pype/modules/clockify/clockify_module.py index 61eaaa5747..a91addb971 100644 --- a/pype/modules/clockify/clockify_module.py +++ b/pype/modules/clockify/clockify_module.py @@ -23,7 +23,7 @@ class ClockifyModule( IFtrackEventHandlerPaths, ITimersManager ): - name = "Clockify" + name = "clockify" def initialize(self, modules_settings): clockify_settings = modules_settings[self.name] diff --git a/pype/modules/ftrack/ftrack_module.py b/pype/modules/ftrack/ftrack_module.py index 03ea4b96a2..2560f7bc0c 100644 --- a/pype/modules/ftrack/ftrack_module.py +++ b/pype/modules/ftrack/ftrack_module.py @@ -21,7 +21,7 @@ class IFtrackEventHandlerPaths: class FtrackModule( PypeModule, ITrayModule, IPluginPaths, ITimersManager, IUserModule ): - name = "Ftrack" + name = "ftrack" def initialize(self, settings): ftrack_settings = settings[self.name] diff --git a/pype/modules/idle_manager/idle_manager.py b/pype/modules/idle_manager/idle_manager.py index 92592f6add..fa6d70d229 100644 --- a/pype/modules/idle_manager/idle_manager.py +++ b/pype/modules/idle_manager/idle_manager.py @@ -41,7 +41,7 @@ class IdleManager(PypeModule, ITrayService): Is able to emit signals at specific time idle. """ label = "Idle Service" - name = "Idle Manager" + name = "idle_manager" def initialize(self, module_settings): idle_man_settings = module_settings[self.name] diff --git a/pype/modules/logging/logging_module.py b/pype/modules/logging/logging_module.py index 2aa13cc118..06101b51a5 100644 --- a/pype/modules/logging/logging_module.py +++ b/pype/modules/logging/logging_module.py @@ -3,7 +3,7 @@ from .. import PypeModule, ITrayModule class LoggingModule(PypeModule, ITrayModule): - name = "Logging" + name = "log_viewer" def initialize(self, modules_settings): logging_settings = modules_settings[self.name] diff --git a/pype/modules/rest_api/rest_api.py b/pype/modules/rest_api/rest_api.py index bd9e7d2a83..2a074fd97a 100644 --- a/pype/modules/rest_api/rest_api.py +++ b/pype/modules/rest_api/rest_api.py @@ -97,7 +97,7 @@ class RestApiModule(PypeModule, ITrayService): `_handle_callback_result` defined in handler. """ label = "Rest API Service" - name = "Rest Api" + name = "rest_api" def initialize(self, modules_settings): rest_api_settings = modules_settings[self.name] diff --git a/pype/modules/settings_module.py b/pype/modules/settings_module.py new file mode 100644 index 0000000000..0651170148 --- /dev/null +++ b/pype/modules/settings_module.py @@ -0,0 +1,53 @@ +from . import PypeModule, ITrayModule + + +class SettingsModule(PypeModule, ITrayModule): + name = "settings" + + def initialize(self, _modules_settings): + # This module is always enabled + self.enabled = True + + # User role + # TODO should be changeable + self.user_role = "developer" + + # Tray attributes + self.settings_window = None + + def connect_with_modules(self, *_a, **_kw): + return + + def create_settings_window(self): + if self.settings_window: + return + from pype.tools.settings import MainWidget + self.settings_window = MainWidget(self.user_role) + + def show_settings_window(self): + if not self.settings_window: + raise AssertionError("Window is not initialized.") + + self.settings_window.show() + + # Pull window to the front. + self.settings_window.raise_() + self.settings_window.activateWindow() + + def tray_init(self): + self.create_settings_window() + + def tray_menu(self, tray_menu): + """Add **change credentials** option to tray menu.""" + from Qt import QtWidgets + + # Actions + action = QtWidgets.QAction("Settings", tray_menu) + action.triggered.connect(self.show_settings_window) + tray_menu.addAction(action) + + def tray_start(self): + return + + def tray_exit(self): + return diff --git a/pype/modules/standalonepublish/standalonepublish_module.py b/pype/modules/standalonepublish/standalonepublish_module.py index abed6bddd9..5b0cfe14bf 100644 --- a/pype/modules/standalonepublish/standalonepublish_module.py +++ b/pype/modules/standalonepublish/standalonepublish_module.py @@ -7,7 +7,7 @@ from .. import PypeModule, ITrayModule class StandAlonePublishModule(PypeModule, ITrayModule): menu_label = "Publish" - name = "Standalone Publish" + name = "standalonepublish_tool" def initialize(self, modules_settings): self.enabled = modules_settings[self.name]["enabled"] diff --git a/pype/modules/timers_manager/timers_manager.py b/pype/modules/timers_manager/timers_manager.py index f4f5243f13..d5b65a7bd7 100644 --- a/pype/modules/timers_manager/timers_manager.py +++ b/pype/modules/timers_manager/timers_manager.py @@ -35,7 +35,7 @@ class TimersManager(PypeModule, ITrayService, IIdleManager): If IdleManager is imported then is able to handle about stop timers when user idles for a long time (set in presets). """ - name = "Timers Manager" + name = "timers_manager" label = "Timers Service" def initialize(self, modules_settings): diff --git a/pype/modules/user/user_module.py b/pype/modules/user/user_module.py index 240ede947b..21e687c43d 100644 --- a/pype/modules/user/user_module.py +++ b/pype/modules/user/user_module.py @@ -27,7 +27,7 @@ class UserModule(PypeModule, ITrayModule, IRestApi): cred_filename = 'user_info.json' env_name = "PYPE_USERNAME" - name = "User setting" + name = "user" def initialize(self, modules_settings): user_settings = modules_settings[self.name] diff --git a/pype/settings/constants.py b/pype/settings/constants.py new file mode 100644 index 0000000000..d21817fb8e --- /dev/null +++ b/pype/settings/constants.py @@ -0,0 +1,34 @@ +# Metadata keys for work with studio and project overrides +M_OVERRIDEN_KEY = "__overriden_keys__" +# Metadata key for storing information about environments +M_ENVIRONMENT_KEY = "__environment_keys__" +# Metadata key for storing dynamic created labels +M_DYNAMIC_KEY_LABEL = "__dynamic_keys_labels__" +# NOTE key popping not implemented yet +M_POP_KEY = "__pop_key__" + +METADATA_KEYS = ( + M_OVERRIDEN_KEY, + M_ENVIRONMENT_KEY, + M_DYNAMIC_KEY_LABEL, + M_POP_KEY +) + +# File where studio's system overrides are stored +SYSTEM_SETTINGS_KEY = "system_settings" +PROJECT_SETTINGS_KEY = "project_settings" +PROJECT_ANATOMY_KEY = "project_anatomy" + + +__all__ = ( + "M_OVERRIDEN_KEY", + "M_ENVIRONMENT_KEY", + "M_DYNAMIC_KEY_LABEL", + "M_POP_KEY", + + "METADATA_KEYS", + + "SYSTEM_SETTINGS_KEY", + "PROJECT_SETTINGS_KEY", + "PROJECT_ANATOMY_KEY" +) diff --git a/pype/settings/defaults/system_settings/modules.json b/pype/settings/defaults/system_settings/modules.json index bd5b3e8294..d123beee14 100644 --- a/pype/settings/defaults/system_settings/modules.json +++ b/pype/settings/defaults/system_settings/modules.json @@ -1,5 +1,5 @@ { - "Avalon": { + "avalon": { "AVALON_MONGO": "", "AVALON_TIMEOUT": 1000, "AVALON_THUMBNAIL_ROOT": { @@ -9,7 +9,7 @@ }, "AVALON_DB_DATA": "{PYPE_SETUP_PATH}/../mongo_db_data" }, - "Ftrack": { + "ftrack": { "enabled": true, "ftrack_server": "https://pype.ftrackapp.com", "ftrack_actions_path": [], @@ -104,16 +104,16 @@ } } }, - "Rest Api": { + "rest_api": { "default_port": 8021, "exclude_ports": [] }, - "Timers Manager": { + "timers_manager": { "enabled": true, "full_time": 15.0, "message_time": 0.5 }, - "Clockify": { + "clockify": { "enabled": false, "workspace_name": "studio name" }, @@ -138,16 +138,16 @@ "ffmpeg": 48 } }, - "Logging": { + "log_viewer": { "enabled": true }, - "User setting": { + "user": { "enabled": true }, - "Standalone Publish": { + "standalonepublish_tool": { "enabled": true }, - "Idle Manager": { + "idle_manager": { "enabled": true } } \ No newline at end of file diff --git a/pype/settings/handlers.py b/pype/settings/handlers.py new file mode 100644 index 0000000000..0df4c98820 --- /dev/null +++ b/pype/settings/handlers.py @@ -0,0 +1,497 @@ +import os +import json +import copy +import logging +import collections +import datetime +from abc import ABCMeta, abstractmethod +import six +import pype +from .constants import ( + SYSTEM_SETTINGS_KEY, + PROJECT_SETTINGS_KEY, + PROJECT_ANATOMY_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 + + +class SettingsFileHandler(SettingsHandler): + def __init__(self): + self.log = logging.getLogger("SettingsFileHandler") + + # Folder where studio overrides are stored + studio_overrides_dir = os.getenv("PYPE_PROJECT_CONFIGS") + if not studio_overrides_dir: + studio_overrides_dir = os.path.dirname(os.path.dirname( + os.path.abspath(pype.__file__) + )) + system_settings_path = os.path.join( + studio_overrides_dir, SYSTEM_SETTINGS_KEY + ".json" + ) + + # File where studio's default project overrides are stored + project_settings_filename = PROJECT_SETTINGS_KEY + ".json" + project_settings_path = os.path.join( + studio_overrides_dir, project_settings_filename + ) + + project_anatomy_filename = PROJECT_ANATOMY_KEY + ".json" + project_anatomy_path = os.path.join( + studio_overrides_dir, project_anatomy_filename + ) + + self.studio_overrides_dir = studio_overrides_dir + self.system_settings_path = system_settings_path + + self.project_settings_filename = project_settings_filename + self.project_anatomy_filename = project_anatomy_filename + + self.project_settings_path = project_settings_path + self.project_anatomy_path = project_anatomy_path + + def path_to_project_settings(self, project_name): + if not project_name: + return self.project_settings_path + return os.path.join( + self.studio_overrides_dir, + project_name, + self.project_settings_filename + ) + + def path_to_project_anatomy(self, project_name): + if not project_name: + return self.project_anatomy_path + return os.path.join( + self.studio_overrides_dir, + project_name, + self.project_anatomy_filename + ) + + 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. + """ + dirpath = os.path.dirname(self.system_settings_path) + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + self.log.debug( + "Saving studio overrides. Output path: {}".format( + self.system_settings_path + ) + ) + with open(self.system_settings_path, "w") as file_stream: + json.dump(data, file_stream, indent=4) + + 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. + """ + project_overrides_json_path = self.path_to_project_settings( + project_name + ) + dirpath = os.path.dirname(project_overrides_json_path) + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + self.log.debug( + "Saving overrides of project \"{}\". Output path: {}".format( + project_name, project_overrides_json_path + ) + ) + with open(project_overrides_json_path, "w") as file_stream: + json.dump(overrides, file_stream, indent=4) + + 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. + """ + project_anatomy_json_path = self.path_to_project_anatomy(project_name) + dirpath = os.path.dirname(project_anatomy_json_path) + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + self.log.debug( + "Saving anatomy of project \"{}\". Output path: {}".format( + project_name, project_anatomy_json_path + ) + ) + with open(project_anatomy_json_path, "w") as file_stream: + json.dump(anatomy_data, file_stream, indent=4) + + def get_studio_system_settings_overrides(self): + """Studio overrides of system settings.""" + if os.path.exists(self.system_settings_path): + return load_json_file(self.system_settings_path) + return {} + + def get_studio_project_settings_overrides(self): + """Studio overrides of default project settings.""" + if os.path.exists(self.project_settings_path): + return load_json_file(self.project_settings_path) + return {} + + def get_studio_project_anatomy_overrides(self): + """Studio overrides of default project anatomy data.""" + if os.path.exists(self.project_anatomy_path): + return load_json_file(self.project_anatomy_path) + return {} + + 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. + """ + path_to_json = self.path_to_project_settings(project_name) + if not os.path.exists(path_to_json): + return {} + return load_json_file(path_to_json) + + 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 {} + + path_to_json = self.path_to_project_anatomy(project_name) + if not os.path.exists(path_to_json): + return {} + return load_json_file(path_to_json) + + +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): + value = "{}" + if document: + value = document.get("value") or value + self.data = json.loads(value) + + 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 pype.lib import PypeMongoConnection + settings_collection = PypeMongoConnection.get_mongo_client() + + # TODO prepare version of pype + # - pype version should define how are settings saved and loaded + + # TODO modify to not use hardcoded keys + database_name = "pype" + 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.system_settings_cache = CacheValues() + self.project_settings_cache = collections.defaultdict(CacheValues) + self.project_anatomy_cache = collections.defaultdict(CacheValues) + + 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. + """ + self.system_settings_cache.update_data(data) + + self.collection.replace_one( + { + "type": SYSTEM_SETTINGS_KEY + }, + { + "type": SYSTEM_SETTINGS_KEY, + "value": self.system_settings_cache.to_json_string() + }, + 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) + + self._save_project_data( + project_name, PROJECT_ANATOMY_KEY, data_cache + ) + + 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, + "value": data_cache.to_json_string(), + "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 _get_project_anatomy_overrides(self, project_name): + if self.project_anatomy_cache[project_name].is_outdated: + document_filter = { + "type": PROJECT_ANATOMY_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_anatomy_cache[project_name].update_from_document( + document + ) + 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) diff --git a/pype/settings/lib.py b/pype/settings/lib.py index dfd4707dbf..fcbe2604d3 100644 --- a/pype/settings/lib.py +++ b/pype/settings/lib.py @@ -1,60 +1,95 @@ import os import json +import functools import logging import copy +from .constants import ( + M_OVERRIDEN_KEY, + M_ENVIRONMENT_KEY, + M_POP_KEY, + + METADATA_KEYS, + + SYSTEM_SETTINGS_KEY, + PROJECT_SETTINGS_KEY, + PROJECT_ANATOMY_KEY +) log = logging.getLogger(__name__) # Py2 + Py3 json decode exception JSON_EXC = getattr(json.decoder, "JSONDecodeError", ValueError) -# Metadata keys for work with studio and project overrides -M_OVERRIDEN_KEY = "__overriden_keys__" -# Metadata key for storing information about environments -M_ENVIRONMENT_KEY = "__environment_keys__" -# Metadata key for storing dynamic created labels -M_DYNAMIC_KEY_LABEL = "__dynamic_keys_labels__" -# NOTE key popping not implemented yet -M_POP_KEY = "__pop_key__" - -METADATA_KEYS = ( - M_OVERRIDEN_KEY, - M_ENVIRONMENT_KEY, - M_DYNAMIC_KEY_LABEL, - M_POP_KEY -) - -# Folder where studio overrides are stored -STUDIO_OVERRIDES_PATH = os.getenv("PYPE_PROJECT_CONFIGS") or "" - -# File where studio's system overrides are stored -SYSTEM_SETTINGS_KEY = "system_settings" -SYSTEM_SETTINGS_PATH = os.path.join( - STUDIO_OVERRIDES_PATH, SYSTEM_SETTINGS_KEY + ".json" -) - -# File where studio's environment overrides are stored -ENVIRONMENTS_KEY = "environments" - -# File where studio's default project overrides are stored -PROJECT_SETTINGS_KEY = "project_settings" -PROJECT_SETTINGS_FILENAME = PROJECT_SETTINGS_KEY + ".json" -PROJECT_SETTINGS_PATH = os.path.join( - STUDIO_OVERRIDES_PATH, PROJECT_SETTINGS_FILENAME -) - -PROJECT_ANATOMY_KEY = "project_anatomy" -PROJECT_ANATOMY_FILENAME = PROJECT_ANATOMY_KEY + ".json" -PROJECT_ANATOMY_PATH = os.path.join( - STUDIO_OVERRIDES_PATH, PROJECT_ANATOMY_FILENAME -) # Path to default settings -DEFAULTS_DIR = os.path.join(os.path.dirname(__file__), "defaults") +DEFAULTS_DIR = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "defaults" +) # Variable where cache of default settings are stored _DEFAULT_SETTINGS = None +# Handler of studio overrides +_SETTINGS_HANDLER = None + + +def require_handler(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + global _SETTINGS_HANDLER + if _SETTINGS_HANDLER is None: + _SETTINGS_HANDLER = create_settings_handler() + return func(*args, **kwargs) + return wrapper + + +def create_settings_handler(): + from .handlers import MongoSettingsHandler + # Handler can't be created in global space on initialization but only when + # needed. Plus here may be logic: Which handler is used (in future). + return MongoSettingsHandler() + + +@require_handler +def save_studio_settings(data): + return _SETTINGS_HANDLER.save_studio_settings(data) + + +@require_handler +def save_project_settings(project_name, overrides): + return _SETTINGS_HANDLER.save_project_settings(project_name, overrides) + + +@require_handler +def save_project_anatomy(project_name, anatomy_data): + return _SETTINGS_HANDLER.save_project_anatomy(project_name, anatomy_data) + + +@require_handler +def get_studio_system_settings_overrides(): + return _SETTINGS_HANDLER.get_studio_system_settings_overrides() + + +@require_handler +def get_studio_project_settings_overrides(): + return _SETTINGS_HANDLER.get_studio_project_settings_overrides() + + +@require_handler +def get_studio_project_anatomy_overrides(): + return _SETTINGS_HANDLER.get_studio_project_anatomy_overrides() + + +@require_handler +def get_project_settings_overrides(project_name): + return _SETTINGS_HANDLER.get_project_settings_overrides(project_name) + + +@require_handler +def get_project_anatomy_overrides(project_name): + return _SETTINGS_HANDLER.get_project_anatomy_overrides(project_name) + class DuplicatedEnvGroups(Exception): def __init__(self, duplicated): @@ -80,10 +115,12 @@ def reset_default_settings(): def get_default_settings(): - global _DEFAULT_SETTINGS - if _DEFAULT_SETTINGS is None: - _DEFAULT_SETTINGS = load_jsons_from_dir(DEFAULTS_DIR) - return copy.deepcopy(_DEFAULT_SETTINGS) + # TODO add cacher + return load_jsons_from_dir(DEFAULTS_DIR) + # global _DEFAULT_SETTINGS + # if _DEFAULT_SETTINGS is None: + # _DEFAULT_SETTINGS = load_jsons_from_dir(DEFAULTS_DIR) + # return copy.deepcopy(_DEFAULT_SETTINGS) def load_json_file(fpath): @@ -246,150 +283,6 @@ def subkey_merge(_dict, value, keys): return _dict -def path_to_project_settings(project_name): - if not project_name: - return PROJECT_SETTINGS_PATH - return os.path.join( - STUDIO_OVERRIDES_PATH, - project_name, - PROJECT_SETTINGS_FILENAME - ) - - -def path_to_project_anatomy(project_name): - if not project_name: - return PROJECT_ANATOMY_PATH - return os.path.join( - STUDIO_OVERRIDES_PATH, - project_name, - PROJECT_ANATOMY_FILENAME - ) - - -def save_studio_settings(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. - """ - dirpath = os.path.dirname(SYSTEM_SETTINGS_PATH) - if not os.path.exists(dirpath): - os.makedirs(dirpath) - - print("Saving studio overrides. Output path: {}".format( - SYSTEM_SETTINGS_PATH - )) - with open(SYSTEM_SETTINGS_PATH, "w") as file_stream: - json.dump(data, file_stream, indent=4) - - -def save_project_settings(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. - """ - project_overrides_json_path = path_to_project_settings(project_name) - dirpath = os.path.dirname(project_overrides_json_path) - if not os.path.exists(dirpath): - os.makedirs(dirpath) - - print("Saving overrides of project \"{}\". Output path: {}".format( - project_name, project_overrides_json_path - )) - with open(project_overrides_json_path, "w") as file_stream: - json.dump(overrides, file_stream, indent=4) - - -def save_project_anatomy(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. - """ - project_anatomy_json_path = path_to_project_anatomy(project_name) - dirpath = os.path.dirname(project_anatomy_json_path) - if not os.path.exists(dirpath): - os.makedirs(dirpath) - - print("Saving anatomy of project \"{}\". Output path: {}".format( - project_name, project_anatomy_json_path - )) - with open(project_anatomy_json_path, "w") as file_stream: - json.dump(anatomy_data, file_stream, indent=4) - - -def get_studio_system_settings_overrides(): - """Studio overrides of system settings.""" - if os.path.exists(SYSTEM_SETTINGS_PATH): - return load_json_file(SYSTEM_SETTINGS_PATH) - return {} - - -def get_studio_project_settings_overrides(): - """Studio overrides of default project settings.""" - if os.path.exists(PROJECT_SETTINGS_PATH): - return load_json_file(PROJECT_SETTINGS_PATH) - return {} - - -def get_studio_project_anatomy_overrides(): - """Studio overrides of default project anatomy data.""" - if os.path.exists(PROJECT_ANATOMY_PATH): - return load_json_file(PROJECT_ANATOMY_PATH) - return {} - - -def get_project_settings_overrides(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. - """ - - path_to_json = path_to_project_settings(project_name) - if not os.path.exists(path_to_json): - return {} - return load_json_file(path_to_json) - - -def get_project_anatomy_overrides(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 {} - - path_to_json = path_to_project_anatomy(project_name) - if not os.path.exists(path_to_json): - return {} - return load_json_file(path_to_json) - - def merge_overrides(source_dict, override_dict): """Merge data from override_dict to source_dict.""" @@ -459,7 +352,9 @@ def get_anatomy_settings(project_name, clear_metadata=True): ) studio_overrides = get_default_anatomy_settings(False) - project_overrides = get_project_anatomy_overrides(project_name) + project_overrides = get_project_anatomy_overrides( + project_name + ) result = apply_overrides(studio_overrides, project_overrides) if clear_metadata: @@ -476,7 +371,9 @@ def get_project_settings(project_name, clear_metadata=True): ) studio_overrides = get_default_project_settings(False) - project_overrides = get_project_settings_overrides(project_name) + project_overrides = get_project_settings_overrides( + project_name + ) result = apply_overrides(studio_overrides, project_overrides) if clear_metadata: diff --git a/pype/tools/settings/__main__.py b/pype/tools/settings/__main__.py index 2e27c28208..7e9f80a52c 100644 --- a/pype/tools/settings/__main__.py +++ b/pype/tools/settings/__main__.py @@ -6,9 +6,6 @@ from Qt import QtWidgets, QtGui if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) - - stylesheet = settings.style.load_stylesheet() - app.setStyleSheet(stylesheet) app.setWindowIcon(QtGui.QIcon(settings.style.app_icon_path())) _develop = "-d" in sys.argv or "--develop" in sys.argv diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/module_settings/schema_ftrack.json b/pype/tools/settings/settings/gui_schemas/system_schema/module_settings/schema_ftrack.json index 5459379bcb..1f501cfde3 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/module_settings/schema_ftrack.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/module_settings/schema_ftrack.json @@ -1,6 +1,6 @@ { "type": "dict", - "key": "Ftrack", + "key": "ftrack", "label": "Ftrack", "collapsable": true, "checkbox_key": "enabled", diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/schema_modules.json b/pype/tools/settings/settings/gui_schemas/system_schema/schema_modules.json index af4426fd01..0b22530caa 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/schema_modules.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/schema_modules.json @@ -6,7 +6,7 @@ "is_file": true, "children": [{ "type": "dict", - "key": "Avalon", + "key": "avalon", "label": "Avalon", "collapsable": true, "children": [{ @@ -40,7 +40,7 @@ }, { "type": "dict", - "key": "Rest Api", + "key": "rest_api", "label": "Rest Api", "collapsable": true, "children": [{ @@ -63,7 +63,7 @@ ] }, { "type": "dict", - "key": "Timers Manager", + "key": "timers_manager", "label": "Timers Manager", "collapsable": true, "checkbox_key": "enabled", @@ -86,7 +86,7 @@ ] }, { "type": "dict", - "key": "Clockify", + "key": "clockify", "label": "Clockify", "collapsable": true, "checkbox_key": "enabled", @@ -144,7 +144,7 @@ }] }, { "type": "dict", - "key": "Logging", + "key": "log_viewer", "label": "Logging", "collapsable": true, "checkbox_key": "enabled", @@ -155,7 +155,7 @@ }] }, { "type": "dict", - "key": "User setting", + "key": "user", "label": "User setting", "collapsable": true, "checkbox_key": "enabled", @@ -166,7 +166,7 @@ }] }, { "type": "dict", - "key": "Standalone Publish", + "key": "standalonepublish_tool", "label": "Standalone Publish", "collapsable": true, "checkbox_key": "enabled", @@ -177,7 +177,7 @@ }] }, { "type": "dict", - "key": "Idle Manager", + "key": "idle_manager", "label": "Idle Manager", "collapsable": true, "checkbox_key": "enabled", diff --git a/pype/tools/settings/settings/widgets/base.py b/pype/tools/settings/settings/widgets/base.py index 4ff7ec26e7..8e16c3614c 100644 --- a/pype/tools/settings/settings/widgets/base.py +++ b/pype/tools/settings/settings/widgets/base.py @@ -2,11 +2,13 @@ import os import copy import json from Qt import QtWidgets, QtCore, QtGui -from pype.settings.lib import ( +from pype.settings.constants import ( SYSTEM_SETTINGS_KEY, PROJECT_SETTINGS_KEY, - PROJECT_ANATOMY_KEY, + PROJECT_ANATOMY_KEY +) +from pype.settings.lib import ( DEFAULTS_DIR, reset_default_settings, diff --git a/pype/tools/settings/settings/widgets/lib.py b/pype/tools/settings/settings/widgets/lib.py index 9a6331009b..9c56f51726 100644 --- a/pype/tools/settings/settings/widgets/lib.py +++ b/pype/tools/settings/settings/widgets/lib.py @@ -2,7 +2,7 @@ import os import re import json import copy -from pype.settings.lib import ( +from pype.settings.constants import ( M_OVERRIDEN_KEY, M_ENVIRONMENT_KEY, M_DYNAMIC_KEY_LABEL diff --git a/pype/tools/settings/settings/widgets/window.py b/pype/tools/settings/settings/widgets/window.py index 670d00fb2b..2dd5111d74 100644 --- a/pype/tools/settings/settings/widgets/window.py +++ b/pype/tools/settings/settings/widgets/window.py @@ -1,5 +1,6 @@ -from Qt import QtWidgets +from Qt import QtWidgets, QtGui from .base import SystemWidget, ProjectWidget +from .. import style class MainWidget(QtWidgets.QWidget): @@ -13,6 +14,10 @@ class MainWidget(QtWidgets.QWidget): self.resize(self.widget_width, self.widget_height) + stylesheet = style.load_stylesheet() + self.setStyleSheet(stylesheet) + self.setWindowIcon(QtGui.QIcon(style.app_icon_path())) + header_tab_widget = QtWidgets.QTabWidget(parent=self) studio_widget = SystemWidget(user_role, header_tab_widget) diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index feba46987f..a22dae32b9 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -62,6 +62,7 @@ class Window(QtWidgets.QDialog): # signals widget_assets.selection_changed.connect(self.on_asset_changed) + widget_assets.project_changed.connect(self.on_project_change) widget_family.stateChanged.connect(self.set_valid_family) self.widget_assets = widget_assets @@ -116,6 +117,9 @@ class Window(QtWidgets.QDialog): parents.append(parent['name']) return parents + def on_project_change(self, project_name): + self.widget_family.refresh() + def on_asset_changed(self): '''Callback on asset selection changed diff --git a/pype/tools/standalonepublish/widgets/widget_asset.py b/pype/tools/standalonepublish/widgets/widget_asset.py index 6f041a535f..91565668cc 100644 --- a/pype/tools/standalonepublish/widgets/widget_asset.py +++ b/pype/tools/standalonepublish/widgets/widget_asset.py @@ -121,6 +121,7 @@ class AssetWidget(QtWidgets.QWidget): """ + project_changed = QtCore.Signal(str) assets_refreshed = QtCore.Signal() # on model refresh selection_changed = QtCore.Signal() # on view selection change current_changed = QtCore.Signal() # on view current index change @@ -249,6 +250,9 @@ class AssetWidget(QtWidgets.QWidget): project_name = self.combo_projects.currentText() if project_name in projects: self.dbcon.Session["AVALON_PROJECT"] = project_name + + self.project_changed.emit(project_name) + self.refresh() def _refresh_model(self): diff --git a/pype/tools/standalonepublish/widgets/widget_family.py b/pype/tools/standalonepublish/widgets/widget_family.py index 5c0c8ccd38..ed30dba420 100644 --- a/pype/tools/standalonepublish/widgets/widget_family.py +++ b/pype/tools/standalonepublish/widgets/widget_family.py @@ -285,7 +285,10 @@ class FamilyWidget(QtWidgets.QWidget): self.schedule(self._on_data_changed, 500, channel="gui") def on_selection_changed(self, *args): - plugin = self.list_families.currentItem().data(PluginRole) + item = self.list_families.currentItem() + if not item: + return + plugin = item.data(PluginRole) if plugin is None: return @@ -309,10 +312,15 @@ class FamilyWidget(QtWidgets.QWidget): """ def refresh(self): + self.list_families.clear() + has_families = False - settings = get_project_settings(os.environ['AVALON_PROJECT']) + project_name = self.dbcon.Session.get("AVALON_PROJECT") + if not project_name: + return + + settings = get_project_settings(project_name) sp_settings = settings.get('standalonepublisher', {}) - print(sp_settings) for key, creator in sp_settings.get("create", {}).items(): if key == "__dynamic_keys_labels__":