diff --git a/dev_mongo.ps1 b/dev_mongo.ps1 new file mode 100644 index 0000000000..9ad021e39d --- /dev/null +++ b/dev_mongo.ps1 @@ -0,0 +1,2 @@ +.\venv\Scripts\Activate.ps1 +python pype.py mongodb diff --git a/dev_settings.ps1 b/dev_settings.ps1 new file mode 100644 index 0000000000..3eab14dc37 --- /dev/null +++ b/dev_settings.ps1 @@ -0,0 +1,2 @@ +.\venv\Scripts\Activate.ps1 +python pype.py settings --dev diff --git a/dev_tray.ps1 b/dev_tray.ps1 new file mode 100644 index 0000000000..44f3f69754 --- /dev/null +++ b/dev_tray.ps1 @@ -0,0 +1,2 @@ +.\venv\Scripts\Activate.ps1 +python pype.py tray --debug diff --git a/pype.py b/pype.py index 7f319f0d32..2580bafb55 100644 --- a/pype.py +++ b/pype.py @@ -60,12 +60,45 @@ def set_environments() -> None: """ # FIXME: remove everything except global - env = load_environments(["global", "avalon"]) + env = load_environments(["global"]) env = acre.merge(env, dict(os.environ)) os.environ.clear() os.environ.update(env) +def set_modules_environments(): + """Set global environments for pype's modules. + + This requires to have pype in `sys.path`. + """ + + from pype.modules import ModulesManager + + modules_manager = ModulesManager() + + module_envs = modules_manager.collect_global_environments() + publish_plugin_dirs = modules_manager.collect_plugin_paths()["publish"] + + # Set pyblish plugins paths if any module want to register them + if publish_plugin_dirs: + publish_paths_str = os.environ.get("PYBLISHPLUGINPATH") or "" + publish_paths = publish_paths_str.split(os.pathsep) + _publish_paths = set() + for path in publish_paths: + if path: + _publish_paths.add(os.path.normpath(path)) + for path in publish_plugin_dirs: + _publish_paths.add(os.path.normpath(path)) + module_envs["PYBLISHPLUGINPATH"] = os.pathsep.join(_publish_paths) + + # Metge environments with current environments and update values + if module_envs: + parsed_envs = acre.parse(module_envs) + env = acre.merge(parsed_envs, dict(os.environ)) + os.environ.clear() + os.environ.update(env) + + def boot(): """Bootstrap Pype.""" art = r""" @@ -106,10 +139,6 @@ def boot(): else: os.environ["PYPE_MONGO"] = pype_mongo - # FIXME (antirotor): we need to set those in different way - if not os.getenv("AVALON_MONGO"): - os.environ["AVALON_MONGO"] = os.environ["PYPE_MONGO"] - if getattr(sys, 'frozen', False): if not pype_versions: import igniter @@ -157,15 +186,7 @@ def boot(): # DEPRECATED: remove when `pype-config` dissolves into Pype for good. # .-=-----------------------=-=. ^ .=-=--------------------------=-. - os.environ["PYPE_CONFIG"] = os.path.join( - os.environ["PYPE_ROOT"], "repos", "pype-config") os.environ["PYPE_MODULE_ROOT"] = os.environ["PYPE_ROOT"] - # ------------------------------------------------------------------ - # HARDCODED: - os.environ["AVALON_DB"] = "avalon" - os.environ["AVALON_LABEL"] = "Pype" - os.environ["AVALON_TIMEOUT"] = "1000" - # .-=-----------------------=-=. v .=-=--------------------------=-. # delete Pype module from cache so it is used from specific version try: @@ -179,6 +200,7 @@ def boot(): from pype.version import __version__ print(">>> loading environments ...") set_environments() + set_modules_environments() info = get_info() info.insert(0, ">>> Using Pype from [ {} ]".format( diff --git a/pype/lib/avalon_context.py b/pype/lib/avalon_context.py index 9a9698cac1..3a18e956d9 100644 --- a/pype/lib/avalon_context.py +++ b/pype/lib/avalon_context.py @@ -7,6 +7,9 @@ import functools from pype.settings import get_project_settings +# avalon module is not imported at the top +# - may not be in path at the time of pype.lib initialization +avalon = None log = logging.getLogger("AvalonContext") @@ -14,8 +17,11 @@ log = logging.getLogger("AvalonContext") def with_avalon(func): @functools.wraps(func) def wrap_avalon(*args, **kwargs): - from avalon import api, io, pipeline # noqa: F401 + global avalon + if avalon is None: + import avalon return func(*args, **kwargs) + return wrap_avalon @with_avalon @@ -30,12 +36,12 @@ def is_latest(representation): """ - version = io.find_one({"_id": representation['parent']}) + version = avalon.io.find_one({"_id": representation['parent']}) if version["type"] == "master_version": return True # Get highest version under the parent - highest_version = io.find_one({ + highest_version = avalon.io.find_one({ "type": "version", "parent": version["parent"] }, sort=[("name", -1)], projection={"name": True}) @@ -57,9 +63,9 @@ def any_outdated(): if representation in checked: continue - representation_doc = io.find_one( + representation_doc = avalon.io.find_one( { - "_id": io.ObjectId(representation), + "_id": avalon.io.ObjectId(representation), "type": "representation" }, projection={"parent": True} @@ -90,7 +96,7 @@ def get_asset(asset_name=None): if not asset_name: asset_name = avalon.api.Session["AVALON_ASSET"] - asset_document = io.find_one({ + asset_document = avalon.io.find_one({ "name": asset_name, "type": "asset" }) @@ -114,9 +120,12 @@ def get_hierarchy(asset_name=None): """ if not asset_name: - asset_name = io.Session.get("AVALON_ASSET", os.environ["AVALON_ASSET"]) + asset_name = avalon.io.Session.get( + "AVALON_ASSET", + os.environ["AVALON_ASSET"] + ) - asset_entity = io.find_one({ + asset_entity = avalon.io.find_one({ "type": 'asset', "name": asset_name }) @@ -135,13 +144,13 @@ def get_hierarchy(asset_name=None): parent_id = entity.get("data", {}).get("visualParent") if not parent_id: break - entity = io.find_one({"_id": parent_id}) + entity = avalon.io.find_one({"_id": parent_id}) hierarchy_items.append(entity["name"]) # Add parents to entity data for next query entity_data = asset_entity.get("data", {}) entity_data["parents"] = hierarchy_items - io.update_many( + avalon.io.update_many( {"_id": asset_entity["_id"]}, {"$set": {"data": entity_data}} ) @@ -160,7 +169,7 @@ def get_linked_assets(asset_entity): (list) of MongoDB documents """ inputs = asset_entity["data"].get("inputs", []) - inputs = [io.find_one({"_id": x}) for x in inputs] + inputs = [avalon.io.find_one({"_id": x}) for x in inputs] return inputs @@ -186,9 +195,9 @@ def get_latest_version(asset_name, subset_name, dbcon=None, project_name=None): if not dbcon: log.debug("Using `avalon.io` for query.") - dbcon = io + dbcon = avalon.io # Make sure is installed - io.install() + dbcon.install() if project_name and project_name != dbcon.Session.get("AVALON_PROJECT"): # `avalon.io` has only `_database` attribute @@ -295,8 +304,8 @@ class BuildWorkfile: }] """ # Get current asset name and entity - current_asset_name = io.Session["AVALON_ASSET"] - current_asset_entity = io.find_one({ + current_asset_name = avalon.io.Session["AVALON_ASSET"] + current_asset_entity = avalon.io.find_one({ "type": "asset", "name": current_asset_name }) @@ -324,7 +333,7 @@ class BuildWorkfile: return # Get current task name - current_task_name = io.Session["AVALON_TASK"] + current_task_name = avalon.io.Session["AVALON_TASK"] # Load workfile presets for task self.build_presets = self.get_build_presets(current_task_name) @@ -425,7 +434,7 @@ class BuildWorkfile: (dict): preset per entered task name """ host_name = avalon.api.registered_host().__name__.rsplit(".", 1)[-1] - presets = get_project_settings(io.Session["AVALON_PROJECT"]) + presets = get_project_settings(avalon.io.Session["AVALON_PROJECT"]) # Get presets for host build_presets = ( presets.get(host_name, {}) @@ -765,7 +774,7 @@ class BuildWorkfile: is_loaded = True except Exception as exc: - if exc == pipeline.IncompatibleLoaderError: + if exc == avalon.pipeline.IncompatibleLoaderError: self.log.info(( "Loader `{}` is not compatible with" " representation `{}`" @@ -829,13 +838,13 @@ class BuildWorkfile: asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} - subsets = list(io.find({ + subsets = list(avalon.io.find({ "type": "subset", "parent": {"$in": asset_entity_by_ids.keys()} })) subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} - sorted_versions = list(io.find({ + sorted_versions = list(avalon.io.find({ "type": "version", "parent": {"$in": subset_entity_by_ids.keys()} }).sort("name", -1)) @@ -849,7 +858,7 @@ class BuildWorkfile: subset_id_with_latest_version.append(subset_id) last_versions_by_id[version["_id"]] = version - repres = io.find({ + repres = avalon.io.find({ "type": "representation", "parent": {"$in": last_versions_by_id.keys()} }) diff --git a/pype/lib/mongo.py b/pype/lib/mongo.py index d6da6dae83..f950572c6e 100644 --- a/pype/lib/mongo.py +++ b/pype/lib/mongo.py @@ -73,7 +73,7 @@ def compose_url(scheme=None, def get_default_components(): - mongo_url = os.environ.get("AVALON_MONGO") + mongo_url = os.environ.get("PYPE_MONGO") if mongo_url is None: raise MongoEnvNotSet( "URL for Mongo logging connection is not set." diff --git a/pype/modules/__init__.py b/pype/modules/__init__.py index aacd541e18..11157f24b1 100644 --- a/pype/modules/__init__.py +++ b/pype/modules/__init__.py @@ -1,6 +1,71 @@ # -*- coding: utf-8 -*- -from .base import PypeModule +from .base import ( + PypeModule, + ITrayModule, + ITrayService, + IPluginPaths, + ModulesManager, + TrayModulesManager +) + +from .rest_api import ( + RestApiModule, + IRestApi +) +from .user import ( + UserModule, + IUserModule +) +from .idle_manager import ( + IdleManager, + IIdleManager +) +from .timers_manager import ( + TimersManager, + ITimersManager +) +from .avalon_apps import AvalonModule +from .ftrack import ( + FtrackModule, + IFtrackEventHandlerPaths +) +from .clockify import ClockifyModule +from .logging import LoggingModule +from .muster import MusterModule +from .standalonepublish import StandAlonePublishModule +from .websocket_server import WebsocketModule + __all__ = ( "PypeModule", + "ITrayModule", + "ITrayService", + "IPluginPaths", + "ModulesManager", + "TrayModulesManager", + + "UserModule", + "IUserModule", + + "IdleManager", + "IIdleManager", + + "TimersManager", + "ITimersManager", + + "RestApiModule", + "IRestApi", + + "AvalonModule", + + "FtrackModule", + "IFtrackEventHandlerPaths", + + "ClockifyModule", + "IdleManager", + "LoggingModule", + "MusterModule", + "StandAlonePublishModule", + + "WebsocketModule" ) diff --git a/pype/modules/avalon_apps/__init__.py b/pype/modules/avalon_apps/__init__.py index 845f94a330..baa21cc803 100644 --- a/pype/modules/avalon_apps/__init__.py +++ b/pype/modules/avalon_apps/__init__.py @@ -1,5 +1,6 @@ -from .avalon_app import AvalonApps +from .avalon_app import AvalonModule -def tray_init(tray_widget, main_widget): - return AvalonApps(main_widget, tray_widget) +__all__ = ( + "AvalonModule", +) diff --git a/pype/modules/avalon_apps/avalon_app.py b/pype/modules/avalon_apps/avalon_app.py index de10268304..d80c0afe6f 100644 --- a/pype/modules/avalon_apps/avalon_app.py +++ b/pype/modules/avalon_apps/avalon_app.py @@ -1,61 +1,153 @@ -from pype.api import Logger +import os +import pype +from pype import resources +from .. import ( + PypeModule, + ITrayModule, + IRestApi +) -class AvalonApps: - def __init__(self, main_parent=None, parent=None): - self.log = Logger().get_logger(__name__) +class AvalonModule(PypeModule, ITrayModule, IRestApi): + name = "Avalon" - self.tray_init(main_parent, parent) + def initialize(self, modules_settings): + # This module is always enabled + self.enabled = True - def tray_init(self, main_parent, parent): - from avalon.tools.libraryloader import app - from avalon import style - from pype.tools.launcher import LauncherWindow, actions + avalon_settings = modules_settings[self.name] - self.parent = parent - self.main_parent = main_parent + # Check if environment is already set + avalon_mongo_url = os.environ.get("AVALON_MONGO") + if not avalon_mongo_url: + avalon_mongo_url = avalon_settings["AVALON_MONGO"] + # Use pype mongo if Avalon's mongo not defined + if not avalon_mongo_url: + avalon_mongo_url = os.environ["PYPE_MONGO"] - self.app_launcher = LauncherWindow() - self.libraryloader = app.Window( - icon=self.parent.icon, - show_projects=True, - show_libraries=True + thumbnail_root = os.environ.get("AVALON_THUMBNAIL_ROOT") + if not thumbnail_root: + thumbnail_root = avalon_settings["AVALON_THUMBNAIL_ROOT"] + + # Mongo timeout + avalon_mongo_timeout = os.environ.get("AVALON_TIMEOUT") + if not avalon_mongo_timeout: + avalon_mongo_timeout = avalon_settings["AVALON_TIMEOUT"] + + self.thumbnail_root = thumbnail_root + self.avalon_mongo_url = avalon_mongo_url + self.avalon_mongo_timeout = avalon_mongo_timeout + + self.schema_path = os.path.join( + os.path.dirname(pype.PACKAGE_DIR), + "schema" ) - self.libraryloader.setStyleSheet(style.load_stylesheet()) - # actions.register_default_actions() - actions.register_config_actions() - actions.register_environment_actions() + # Tray attributes + self.app_launcher = None + self.libraryloader = None + self.rest_api_obj = None - def process_modules(self, modules): - if "RestApiServer" in modules: + def get_global_environments(self): + """Avalon global environments for pype implementation.""" + mongodb_data_dir = os.environ.get("AVALON_DB_DATA") + if not mongodb_data_dir: + mongodb_data_dir = os.path.join( + os.path.dirname(os.environ["PYPE_ROOT"]), + "mongo_db_data" + ) + return { + # 100% hardcoded + "AVALON_SCHEMA": self.schema_path, + "AVALON_CONFIG": "pype", + "AVALON_LABEL": "Pype", + + # Modifiable by settings + # - mongo ulr for avalon projects + "AVALON_MONGO": self.avalon_mongo_url, + # TODO thumbnails root should be multiplafrom + # - thumbnails root + "AVALON_THUMBNAIL_ROOT": self.thumbnail_root, + # - mongo timeout in ms + "AVALON_TIMEOUT": str(self.avalon_mongo_timeout), + + # May be modifiable? + # - mongo database name where projects are stored + "AVALON_DB": "avalon", + + # Not even connected to Avalon + # TODO remove - pype's variable for local mongo + "AVALON_DB_DATA": mongodb_data_dir + } + + def tray_init(self): + # Add library tool + try: + from avalon.tools.libraryloader import app + from avalon import style + from Qt import QtGui + + self.libraryloader = app.Window( + icon=QtGui.QIcon(resources.pype_icon_filepath()), + show_projects=True, + show_libraries=True + ) + self.libraryloader.setStyleSheet(style.load_stylesheet()) + except Exception: + self.log.warning( + "Couldn't load Library loader tool for tray.", + exc_info=True + ) + + # Add launcher + try: + from pype.tools.launcher import LauncherWindow + self.app_launcher = LauncherWindow() + except Exception: + self.log.warning( + "Couldn't load Launch for tray.", + exc_info=True + ) + + def connect_with_modules(self, _enabled_modules): + plugin_paths = self.manager.collect_plugin_paths()["actions"] + if plugin_paths: + env_paths_str = os.environ.get("AVALON_ACTIONS") or "" + env_paths = env_paths_str.split(os.pathsep) + env_paths.extend(plugin_paths) + os.environ["AVALON_ACTIONS"] = os.pathsep.join(env_paths) + + if self.tray_initialized: + from pype.tools.launcher import actions + # actions.register_default_actions() + actions.register_config_actions() + actions.register_environment_actions() + + def rest_api_initialization(self, rest_api_module): + if self.tray_initialized: from .rest_api import AvalonRestApi self.rest_api_obj = AvalonRestApi() # Definition of Tray menu - def tray_menu(self, parent_menu=None): + def tray_menu(self, tray_menu): from Qt import QtWidgets # Actions - if parent_menu is None: - if self.parent is None: - self.log.warning('Parent menu is not set') - return - elif self.parent.hasattr('menu'): - parent_menu = self.parent.menu - else: - self.log.warning('Parent menu is not set') - return - - action_launcher = QtWidgets.QAction("Launcher", parent_menu) + action_launcher = QtWidgets.QAction("Launcher", tray_menu) action_library_loader = QtWidgets.QAction( - "Library loader", parent_menu + "Library loader", tray_menu ) action_launcher.triggered.connect(self.show_launcher) action_library_loader.triggered.connect(self.show_library_loader) - parent_menu.addAction(action_launcher) - parent_menu.addAction(action_library_loader) + tray_menu.addAction(action_launcher) + tray_menu.addAction(action_library_loader) + + def tray_start(self, *_a, **_kw): + return + + def tray_exit(self, *_a, **_kw): + return def show_launcher(self): # if app_launcher don't exist create it/otherwise only show main window diff --git a/pype/modules/base.py b/pype/modules/base.py index ee90aa4cbb..72d0eb4503 100644 --- a/pype/modules/base.py +++ b/pype/modules/base.py @@ -1,38 +1,486 @@ # -*- coding: utf-8 -*- """Base class for Pype Modules.""" +import inspect +import logging from uuid import uuid4 -from abc import ABC, abstractmethod -from pype.api import Logger +from abc import ABCMeta, abstractmethod +import six + +import pype +from pype.settings import get_system_settings +from pype.lib import PypeLogger +from pype import resources -class PypeModule(ABC): +@six.add_metaclass(ABCMeta) +class PypeModule: """Base class of pype module. Attributes: - id (UUID): Module id. + id (UUID): Module's id. enabled (bool): Is module enabled. name (str): Module name. + manager (ModulesManager): Manager that created the module. """ + # Disable by default enabled = False - name = None _id = None - def __init__(self, settings): - if self.name is None: - self.name = self.__class__.__name__ + @property + @abstractmethod + def name(self): + """Module's name.""" + pass - self.log = Logger().get_logger(self.name) + def __init__(self, manager, settings): + self.manager = manager - self.settings = settings.get(self.name) - self.enabled = settings.get("enabled", False) - self._id = uuid4() + self.log = PypeLogger().get_logger(self.name) + + self.initialize(settings) @property def id(self): + if self._id is None: + self._id = uuid4() return self._id @abstractmethod - def startup_environments(self): - """Get startup environments for module.""" + def initialize(self, module_settings): + """Initialization of module attributes. + + It is not recommended to override __init__ that's why specific method + was implemented. + """ + pass + + @abstractmethod + def connect_with_modules(self, enabled_modules): + """Connect with other enabled modules.""" + pass + + def get_global_environments(self): + """Get global environments values of module. + + Environment variables that can be get only from system settings. + """ return {} + + +@six.add_metaclass(ABCMeta) +class IPluginPaths: + """Module has plugin paths to return. + + Expected result is dictionary with keys "publish", "create", "load" or + "actions" and values as list or string. + { + "publish": ["path/to/publish_plugins"] + } + """ + # TODO validation of an output + @abstractmethod + def get_plugin_paths(self): + pass + + +@six.add_metaclass(ABCMeta) +class ITrayModule: + """Module has special procedures when used in Pype Tray. + + IMPORTANT: + The module still must be usable if is not used in tray even if + would do nothing. + """ + tray_initialized = False + + @abstractmethod + def tray_init(self): + """Initialization part of tray implementation. + + Triggered between `initialization` and `connect_with_modules`. + + This is where GUIs should be loaded or tray specific parts should be + prepared. + """ + pass + + @abstractmethod + def tray_menu(self, tray_menu): + """Add module's action to tray menu.""" + pass + + @abstractmethod + def tray_start(self): + """Start procedure in Pype tray.""" + pass + + @abstractmethod + def tray_exit(self): + """Cleanup method which is executed on tray shutdown. + + This is place where all threads should be shut. + """ + pass + + +class ITrayService(ITrayModule): + # Module's property + menu_action = None + + # Class properties + _services_submenu = None + _icon_failed = None + _icon_running = None + _icon_idle = None + + @property + @abstractmethod + def label(self): + """Service label showed in menu.""" + pass + + # TODO be able to get any sort of information to show/print + # @abstractmethod + # def get_service_info(self): + # pass + + @staticmethod + def services_submenu(tray_menu): + if ITrayService._services_submenu is None: + from Qt import QtWidgets + services_submenu = QtWidgets.QMenu("Services", tray_menu) + services_submenu.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) + + @staticmethod + def _load_service_icons(): + from Qt import QtGui + ITrayService._failed_icon = QtGui.QIcon( + resources.get_resource("icons", "circle_red.png") + ) + ITrayService._icon_running = QtGui.QIcon( + resources.get_resource("icons", "circle_green.png") + ) + ITrayService._icon_idle = QtGui.QIcon( + resources.get_resource("icons", "circle_orange.png") + ) + + @staticmethod + def get_icon_running(): + if ITrayService._icon_running is None: + ITrayService._load_service_icons() + return ITrayService._icon_running + + @staticmethod + def get_icon_idle(): + if ITrayService._icon_idle is None: + ITrayService._load_service_icons() + return ITrayService._icon_idle + + @staticmethod + def get_icon_failed(): + if ITrayService._failed_icon is None: + ITrayService._load_service_icons() + return ITrayService._failed_icon + + def tray_menu(self, tray_menu): + from Qt import QtWidgets + action = QtWidgets.QAction( + self.label, + self.services_submenu(tray_menu) + ) + self.menu_action = action + + self.add_service_action(action) + + self.set_service_running_icon() + + def set_service_running_icon(self): + """Change icon of an QAction to green circle.""" + if self.menu_action: + self.menu_action.setIcon(self.get_icon_running()) + + def set_service_failed_icon(self): + """Change icon of an QAction to red circle.""" + if self.menu_action: + self.menu_action.setIcon(self.get_icon_failed()) + + def set_service_idle_icon(self): + """Change icon of an QAction to orange circle.""" + if self.menu_action: + self.menu_action.setIcon(self.get_icon_idle()) + + +class ModulesManager: + def __init__(self): + self.log = logging.getLogger(self.__class__.__name__) + + self.modules = [] + self.modules_by_id = {} + self.modules_by_name = {} + + self.initialize_modules() + self.connect_modules() + + def initialize_modules(self): + """Import and initialize modules.""" + self.log.debug("*** Pype modules initialization.") + # Prepare settings for modules + modules_settings = get_system_settings()["modules"] + # Go through globals in `pype.modules` + for name in dir(pype.modules): + modules_item = getattr(pype.modules, name, None) + # Filter globals that are not classes which inherit from PypeModule + if ( + not inspect.isclass(modules_item) + or modules_item is pype.modules.PypeModule + or not issubclass(modules_item, pype.modules.PypeModule) + ): + continue + + # Check if class is abstract (Developing purpose) + if inspect.isabstract(modules_item): + # Find missing implementations by convetion on `abc` module + not_implemented = [] + for attr_name in dir(modules_item): + attr = getattr(modules_item, attr_name, None) + if attr and getattr(attr, "__isabstractmethod__", None): + not_implemented.append(attr_name) + + # Log missing implementations + self.log.warning(( + "Skipping abstract Class: {}. Missing implementations: {}" + ).format(name, ", ".join(not_implemented))) + continue + + try: + # Try initialize module + module = modules_item(self, modules_settings) + # Store initialized object + self.modules.append(module) + self.modules_by_id[module.id] = module + self.modules_by_name[module.name] = module + enabled_str = "X" + if not module.enabled: + enabled_str = " " + self.log.debug("[{}] {}".format(enabled_str, name)) + + except Exception: + self.log.warning( + "Initialization of module {} failed.".format(name), + exc_info=True + ) + + def connect_modules(self): + """Trigger connection with other enabled modules. + + Modules should handle their interfaces in `connect_with_modules`. + """ + enabled_modules = self.get_enabled_modules() + self.log.debug("Has {} enabled modules.".format(len(enabled_modules))) + for module in enabled_modules: + module.connect_with_modules(enabled_modules) + + def get_enabled_modules(self): + """Enabled modules initialized by the manager. + + Returns: + list: Initialized and enabled modules. + """ + return [ + module + for module in self.modules + if module.enabled + ] + + def collect_global_environments(self): + """Helper to collect global enviornment variabled from modules. + + Returns: + dict: Global environment variables from enabled modules. + + Raises: + AssertionError: Gobal environment variables must be unique for + all modules. + """ + module_envs = {} + for module in self.get_enabled_modules(): + # Collect global module's global environments + _envs = module.get_global_environments() + for key, value in _envs.items(): + if key in module_envs: + # TODO better error message + raise AssertionError( + "Duplicated environment key {}".format(key) + ) + module_envs[key] = value + return module_envs + + def collect_plugin_paths(self): + """Helper to collect all plugins from modules inherited IPluginPaths. + + Unknown keys are logged out. + + Returns: + dict: Output is dictionary with keys "publish", "create", "load" + and "actions" each containing list of paths. + """ + # Output structure + output = { + "publish": [], + "create": [], + "load": [], + "actions": [] + } + unknown_keys_by_module = {} + for module in self.get_enabled_modules(): + # Skip module that do not inherit from `IPluginPaths` + if not isinstance(module, IPluginPaths): + continue + plugin_paths = module.get_plugin_paths() + for key, value in plugin_paths.items(): + # Filter unknown keys + if key not in output: + if module.name not in unknown_keys_by_module: + unknown_keys_by_module[module.name] = [] + unknown_keys_by_module[module.name].append(key) + continue + + # Skip if value is empty + if not value: + continue + + # Convert to list if value is not list + if not isinstance(value, (list, tuple, set)): + value = [value] + output[key].extend(value) + + # Report unknown keys (Developing purposes) + if unknown_keys_by_module: + expected_keys = ", ".join([ + "\"{}\"".format(key) for key in output.keys() + ]) + msg_template = "Module: \"{}\" - got key {}" + msg_items = [] + for module_name, keys in unknown_keys_by_module.items(): + joined_keys = ", ".join([ + "\"{}\"".format(key) for key in keys + ]) + msg_items.append(msg_template.format(module_name, joined_keys)) + self.log.warning(( + "Expected keys from `get_plugin_paths` are {}. {}" + ).format(expected_keys, " | ".join(msg_items))) + return output + + +class TrayModulesManager(ModulesManager): + # Define order of modules in menu + modules_menu_order = ( + "User setting", + "Ftrack", + "muster", + "Avalon", + "Clockify", + "Standalone Publish", + "Logging" + ) + + def __init__(self): + self.log = PypeLogger().get_logger(self.__class__.__name__) + + self.modules = [] + self.modules_by_id = {} + self.modules_by_name = {} + + def initialize(self, tray_menu): + self.initialize_modules() + self.tray_init() + self.connect_modules() + self.tray_menu(tray_menu) + + def get_enabled_tray_modules(self): + output = [] + for module in self.modules: + if module.enabled and isinstance(module, ITrayModule): + output.append(module) + return output + + def tray_init(self): + for module in self.get_enabled_tray_modules(): + try: + module.tray_init() + module.tray_initialized = True + except Exception: + self.log.warning( + "Module \"{}\" crashed on `tray_init`.".format( + module.name + ), + exc_info=True + ) + + def tray_menu(self, tray_menu): + ordered_modules = [] + enabled_by_name = { + module.name: module + for module in self.get_enabled_tray_modules() + } + + for name in self.modules_menu_order: + module_by_name = enabled_by_name.pop(name, None) + if module_by_name: + ordered_modules.append(module_by_name) + ordered_modules.extend(enabled_by_name.values()) + + for module in ordered_modules: + if not module.tray_initialized: + continue + + try: + module.tray_menu(tray_menu) + except Exception: + # Unset initialized mark + module.tray_initialized = False + self.log.warning( + "Module \"{}\" crashed on `tray_menu`.".format( + module.name + ), + exc_info=True + ) + + def start_modules(self): + for module in self.get_enabled_tray_modules(): + if not module.tray_initialized: + if isinstance(module, ITrayService): + module.set_service_failed_icon() + continue + + try: + module.tray_start() + except Exception: + self.log.warning( + "Module \"{}\" crashed on `tray_start`.".format( + module.name + ), + exc_info=True + ) + + def on_exit(self): + for module in self.get_enabled_tray_modules(): + if module.tray_initialized: + try: + module.tray_exit() + except Exception: + self.log.warning( + "Module \"{}\" crashed on `tray_exit`.".format( + module.name + ), + exc_info=True + ) diff --git a/pype/modules/clockify/__init__.py b/pype/modules/clockify/__init__.py index 8e11d2f5f4..98834b516c 100644 --- a/pype/modules/clockify/__init__.py +++ b/pype/modules/clockify/__init__.py @@ -1,7 +1,5 @@ -from .clockify import ClockifyModule +from .clockify_module import ClockifyModule -CLASS_DEFINIION = ClockifyModule - - -def tray_init(tray_widget, main_widget): - return ClockifyModule(main_widget, tray_widget) +__all__ = ( + "ClockifyModule", +) diff --git a/pype/modules/clockify/clockify.py b/pype/modules/clockify/clockify_module.py similarity index 74% rename from pype/modules/clockify/clockify.py rename to pype/modules/clockify/clockify_module.py index 4309bff9f2..61eaaa5747 100644 --- a/pype/modules/clockify/clockify.py +++ b/pype/modules/clockify/clockify_module.py @@ -2,37 +2,55 @@ import os import threading import time -from pype.api import Logger from .clockify_api import ClockifyAPI -from .constants import CLOCKIFY_FTRACK_USER_PATH +from .constants import ( + CLOCKIFY_FTRACK_USER_PATH, + CLOCKIFY_FTRACK_SERVER_PATH +) +from pype.modules import ( + PypeModule, + ITrayModule, + IPluginPaths, + IFtrackEventHandlerPaths, + ITimersManager +) -class ClockifyModule: - workspace_name = None +class ClockifyModule( + PypeModule, + ITrayModule, + IPluginPaths, + IFtrackEventHandlerPaths, + ITimersManager +): + name = "Clockify" - def __init__(self, main_parent=None, parent=None): - if not self.workspace_name: - raise Exception("Clockify Workspace is not set in config.") + def initialize(self, modules_settings): + clockify_settings = modules_settings[self.name] + self.enabled = clockify_settings["enabled"] + self.workspace_name = clockify_settings["workspace_name"] - os.environ["CLOCKIFY_WORKSPACE"] = self.workspace_name + if self.enabled and not self.workspace_name: + raise Exception("Clockify Workspace is not set in settings.") self.timer_manager = None self.MessageWidgetClass = None + self.message_widget = None self.clockapi = ClockifyAPI(master_parent=self) - self.log = Logger().get_logger(self.__class__.__name__, "PypeTray") - self.tray_init(main_parent, parent) + def get_global_environments(self): + return { + "CLOCKIFY_WORKSPACE": self.workspace_name + } - def tray_init(self, main_parent, parent): + def tray_init(self): from .widgets import ClockifySettings, MessageWidget self.MessageWidgetClass = MessageWidget - self.main_parent = main_parent - self.parent = parent self.message_widget = None - self.widget_settings = ClockifySettings(main_parent, self) + self.widget_settings = ClockifySettings(self.clockapi) self.widget_settings_required = None self.thread_timer_check = None @@ -56,43 +74,33 @@ class ClockifyModule: self.set_menu_visibility() - def process_modules(self, modules): - if 'FtrackModule' in modules: - current = os.environ.get('FTRACK_ACTIONS_PATH', '') - if current: - current += os.pathsep - os.environ['FTRACK_ACTIONS_PATH'] = ( - current + CLOCKIFY_FTRACK_USER_PATH - ) + def tray_exit(self, *_a, **_kw): + return - if 'AvalonApps' in modules: - actions_path = os.path.join( - os.path.dirname(__file__), - 'launcher_actions' - ) - current = os.environ.get('AVALON_ACTIONS', '') - if current: - current += os.pathsep - os.environ['AVALON_ACTIONS'] = current + actions_path + def get_plugin_paths(self): + """Implementaton of IPluginPaths to get plugin paths.""" + actions_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "launcher_actions" + ) + return { + "actions": [actions_path] + } - if 'TimersManager' in modules: - self.timer_manager = modules['TimersManager'] - self.timer_manager.add_module(self) + def get_event_handler_paths(self): + """Implementaton of IFtrackEventHandlerPaths to get plugin paths.""" + return { + "user": [CLOCKIFY_FTRACK_USER_PATH], + "server": [CLOCKIFY_FTRACK_SERVER_PATH] + } - def start_timer_manager(self, data): - self.start_timer(data) + def connect_with_modules(self, *_a, **_kw): + return - def stop_timer_manager(self): - self.stop_timer() - - def timer_started(self, data): - if self.timer_manager: - self.timer_manager.start_timers(data) - - def timer_stopped(self): + def clockify_timer_stopped(self): self.bool_timer_run = False - if self.timer_manager: - self.timer_manager.stop_timers() + # Call `ITimersManager` method + self.timer_stopped() def start_timer_check(self): self.bool_thread_check_running = True @@ -110,7 +118,6 @@ class ClockifyModule: self.thread_timer_check = None def check_running(self): - while self.bool_thread_check_running is True: bool_timer_run = False if self.clockapi.get_in_progress() is not None: @@ -118,7 +125,7 @@ class ClockifyModule: if self.bool_timer_run != bool_timer_run: if self.bool_timer_run is True: - self.timer_stopped() + self.clockify_timer_stopped() elif self.bool_timer_run is False: actual_timer = self.clockapi.get_in_progress() if not actual_timer: @@ -151,7 +158,7 @@ class ClockifyModule: "project_name": project_name, "task_type": task_type } - + # Call `ITimersManager` method self.timer_started(data) self.bool_timer_run = bool_timer_run @@ -159,9 +166,8 @@ class ClockifyModule: time.sleep(5) def stop_timer(self): + """Implementation of ITimersManager.""" self.clockapi.finish_time_entry() - if self.bool_timer_run: - self.timer_stopped() def signed_in(self): if not self.timer_manager: @@ -174,6 +180,7 @@ class ClockifyModule: self.start_timer_manager(self.timer_manager.last_task) def start_timer(self, input_data): + """Implementation of ITimersManager.""" # If not api key is not entered then skip if not self.clockapi.get_api_key(): return @@ -198,20 +205,20 @@ class ClockifyModule: "Project \"{}\" was not found in Clockify. Timer won't start." ).format(project_name)) + if not self.MessageWidgetClass: + return + msg = ( "Project \"{}\" is not" " in Clockify Workspace \"{}\"." "

Please inform your Project Manager." ).format(project_name, str(self.clockapi.workspace_name)) - if self.MessageWidgetClass: - self.message_widget = self.MessageWidgetClass( - self.main_parent, msg, "Clockify - Info Message" - ) - self.message_widget.closed.connect( - self.on_message_widget_close - ) - self.message_widget.show() + self.message_widget = self.MessageWidgetClass( + msg, "Clockify - Info Message" + ) + self.message_widget.closed.connect(self.on_message_widget_close) + self.message_widget.show() return diff --git a/pype/modules/clockify/widgets.py b/pype/modules/clockify/widgets.py index dc57a48ecb..718e6668bd 100644 --- a/pype/modules/clockify/widgets.py +++ b/pype/modules/clockify/widgets.py @@ -1,6 +1,6 @@ from Qt import QtCore, QtGui, QtWidgets from avalon import style -from pype.api import resources +from pype import resources class MessageWidget(QtWidgets.QWidget): @@ -10,18 +10,12 @@ class MessageWidget(QtWidgets.QWidget): closed = QtCore.Signal() - def __init__(self, parent=None, messages=[], title="Message"): - + def __init__(self, messages, title): super(MessageWidget, self).__init__() - self._parent = parent - # Icon - if parent and hasattr(parent, 'icon'): - self.setWindowIcon(parent.icon) - else: - icon = QtGui.QIcon(resources.pype_icon_filepath()) - self.setWindowIcon(icon) + icon = QtGui.QIcon(resources.pype_icon_filepath()) + self.setWindowIcon(icon) self.setWindowFlags( QtCore.Qt.WindowCloseButtonHint | @@ -93,30 +87,21 @@ class MessageWidget(QtWidgets.QWidget): class ClockifySettings(QtWidgets.QWidget): - SIZE_W = 300 SIZE_H = 130 loginSignal = QtCore.Signal(object, object, object) - def __init__(self, main_parent=None, parent=None, optional=True): - + def __init__(self, clockapi, optional=True): super(ClockifySettings, self).__init__() - self.parent = parent - self.main_parent = main_parent - self.clockapi = parent.clockapi + self.clockapi = clockapi self.optional = optional self.validated = False # Icon - if hasattr(parent, 'icon'): - self.setWindowIcon(self.parent.icon) - elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'): - self.setWindowIcon(self.parent.parent.icon) - else: - icon = QtGui.QIcon(resources.pype_icon_filepath()) - self.setWindowIcon(icon) + icon = QtGui.QIcon(resources.pype_icon_filepath()) + self.setWindowIcon(icon) self.setWindowFlags( QtCore.Qt.WindowCloseButtonHint | diff --git a/pype/modules/deadline/__init__.py b/pype/modules/deadline/__init__.py new file mode 100644 index 0000000000..5631e501d8 --- /dev/null +++ b/pype/modules/deadline/__init__.py @@ -0,0 +1,6 @@ +from .deadline_module import DeadlineModule + + +__all__ = ( + "DeadlineModule", +) diff --git a/pype/modules/deadline/deadline_module.py b/pype/modules/deadline/deadline_module.py new file mode 100644 index 0000000000..6de68c390f --- /dev/null +++ b/pype/modules/deadline/deadline_module.py @@ -0,0 +1,20 @@ +from .. import PypeModule + + +class DeadlineModule(PypeModule): + name = "deadline" + + def initialize(self, modules_settings): + # This module is always enabled + deadline_settings = modules_settings[self.name] + self.enabled = deadline_settings["enabled"] + self.deadline_url = deadline_settings["DEADLINE_REST_URL"] + + def get_global_environments(self): + """Deadline global environments for pype implementation.""" + return { + "DEADLINE_REST_URL": self.deadline_url + } + + def connect_with_modules(self, *_a, **_kw): + return diff --git a/pype/modules/ftrack/__init__.py b/pype/modules/ftrack/__init__.py index cd3bee216f..c02b0fca19 100644 --- a/pype/modules/ftrack/__init__.py +++ b/pype/modules/ftrack/__init__.py @@ -1,16 +1,15 @@ -import os - +from .ftrack_module import ( + FtrackModule, + IFtrackEventHandlerPaths +) from . import ftrack_server from .ftrack_server import FtrackServer, check_ftrack_url from .lib import BaseHandler, BaseEvent, BaseAction, ServerAction -from pype.api import get_system_settings - -# TODO: set in ftrack module -os.environ["FTRACK_SERVER"] = ( - get_system_settings()["modules"]["Ftrack"]["ftrack_server"] -) __all__ = ( + "FtrackModule", + "IFtrackEventHandlerPaths", + "ftrack_server", "FtrackServer", "check_ftrack_url", diff --git a/pype/modules/ftrack/ftrack_module.py b/pype/modules/ftrack/ftrack_module.py new file mode 100644 index 0000000000..03ea4b96a2 --- /dev/null +++ b/pype/modules/ftrack/ftrack_module.py @@ -0,0 +1,102 @@ +import os +from abc import ABCMeta, abstractmethod +import six +import pype +from pype.modules import ( + PypeModule, ITrayModule, IPluginPaths, ITimersManager, IUserModule +) + + +@six.add_metaclass(ABCMeta) +class IFtrackEventHandlerPaths: + """Other modules interface to return paths to ftrack event handlers. + + Expected output is dictionary with "server" and "user" keys. + """ + @abstractmethod + def get_event_handler_paths(self): + pass + + +class FtrackModule( + PypeModule, ITrayModule, IPluginPaths, ITimersManager, IUserModule +): + name = "Ftrack" + + def initialize(self, settings): + ftrack_settings = settings[self.name] + + self.enabled = ftrack_settings["enabled"] + self.ftrack_url = ftrack_settings["ftrack_server"] + + # TODO load from settings + self.server_event_handlers_paths = [] + self.user_event_handlers_paths = [] + + # Prepare attribute + self.tray_module = None + + def get_global_environments(self): + """Ftrack's global environments.""" + return { + "FTRACK_SERVER": self.ftrack_url + } + + def get_plugin_paths(self): + """Ftrack plugin paths.""" + return { + "publish": [os.path.join(pype.PLUGINS_DIR, "ftrack", "publish")] + } + + def connect_with_modules(self, enabled_modules): + for module in enabled_modules: + if not isinstance(module, IFtrackEventHandlerPaths): + continue + paths_by_type = module.get_event_handler_paths() or {} + for key, value in paths_by_type.items(): + if not value: + continue + + if key not in ("server", "user"): + self.log.warning( + "Unknown event handlers key \"{}\" skipping.".format( + key + ) + ) + continue + + if not isinstance(value, (list, tuple, set)): + value = [value] + + if key == "server": + self.server_event_handlers_paths.extend(value) + elif key == "user": + self.user_event_handlers_paths.extend(value) + + def start_timer(self, data): + """Implementation of ITimersManager interface.""" + if self.tray_module: + self.tray_module.start_timer_manager(data) + + def stop_timer(self): + """Implementation of ITimersManager interface.""" + if self.tray_module: + self.tray_module.stop_timer_manager() + + def on_pype_user_change(self, username): + """Implementation of IUserModule interface.""" + if self.tray_module: + self.tray_module.changed_user() + + def tray_init(self): + from .tray import FtrackTrayWrapper + self.tray_module = FtrackTrayWrapper(self) + + def tray_menu(self, parent_menu): + return self.tray_module.tray_menu(parent_menu) + + def tray_start(self): + return self.tray_module.validate() + + def tray_exit(self): + return self.tray_module.stop_action_server() diff --git a/pype/modules/ftrack/ftrack_server/custom_db_connector.py b/pype/modules/ftrack/ftrack_server/custom_db_connector.py index 8a8ba4ccbb..f435086e8a 100644 --- a/pype/modules/ftrack/ftrack_server/custom_db_connector.py +++ b/pype/modules/ftrack/ftrack_server/custom_db_connector.py @@ -52,11 +52,11 @@ def check_active_collection(func): class CustomDbConnector: log = logging.getLogger(__name__) - timeout = int(os.environ["AVALON_TIMEOUT"]) def __init__( self, uri, database_name, port=None, collection_name=None ): + self.timeout = int(os.environ["AVALON_TIMEOUT"]) self._mongo_client = None self._sentry_client = None self._sentry_logging_handler = None diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py index 436e15f497..08c77d89a2 100644 --- a/pype/modules/ftrack/ftrack_server/lib.py +++ b/pype/modules/ftrack/ftrack_server/lib.py @@ -127,14 +127,15 @@ class StorerEventHub(SocketBaseEventHub): class ProcessEventHub(SocketBaseEventHub): - hearbeat_msg = b"processor" - uri, port, database, collection_name = get_ftrack_event_mongo_info() is_collection_created = False pypelog = Logger().get_logger("Session Processor") def __init__(self, *args, **kwargs): + self.uri, self.port, self.database, self.collection_name = ( + get_ftrack_event_mongo_info() + ) self.dbcon = CustomDbConnector( self.uri, self.database, diff --git a/pype/modules/ftrack/tray/__init__.py b/pype/modules/ftrack/tray/__init__.py index bca0784c7f..b7159197e1 100644 --- a/pype/modules/ftrack/tray/__init__.py +++ b/pype/modules/ftrack/tray/__init__.py @@ -1,5 +1,6 @@ -from .ftrack_module import FtrackModule +from .ftrack_tray import FtrackTrayWrapper -def tray_init(tray_widget, main_widget): - return FtrackModule(main_widget, tray_widget) +__all__ = ( + "FtrackTrayWrapper", +) diff --git a/pype/modules/ftrack/tray/ftrack_module.py b/pype/modules/ftrack/tray/ftrack_tray.py similarity index 93% rename from pype/modules/ftrack/tray/ftrack_module.py rename to pype/modules/ftrack/tray/ftrack_tray.py index 36ce1eec9f..56133208c2 100644 --- a/pype/modules/ftrack/tray/ftrack_module.py +++ b/pype/modules/ftrack/tray/ftrack_tray.py @@ -10,15 +10,15 @@ from ..ftrack_server import socket_thread from ..lib import credentials from . import login_dialog -from pype.api import Logger, resources, get_system_settings +from pype.api import Logger, resources log = Logger().get_logger("FtrackModule", "ftrack") -class FtrackModule: - def __init__(self, main_parent=None, parent=None): - self.parent = parent +class FtrackTrayWrapper: + def __init__(self, module): + self.module = module self.thread_action_server = None self.thread_socket_server = None @@ -29,13 +29,12 @@ class FtrackModule: self.bool_action_thread_running = False self.bool_timer_event = False - self.load_ftrack_url() - self.widget_login = login_dialog.CredentialsDialog() self.widget_login.login_changed.connect(self.on_login_change) self.widget_login.logout_signal.connect(self.on_logout) self.action_credentials = None + self.tray_server_menu = None self.icon_logged = QtGui.QIcon( resources.get_resource("icons", "circle_green.png") ) @@ -118,7 +117,8 @@ class FtrackModule: self.bool_action_server_running = True self.bool_action_thread_running = False - ftrack_url = os.environ['FTRACK_SERVER'] + ftrack_url = self.module.ftrack_url + os.environ["FTRACK_SERVER"] = ftrack_url parent_file_path = os.path.dirname( os.path.dirname(os.path.realpath(__file__)) @@ -294,15 +294,6 @@ class FtrackModule: def tray_exit(self): self.stop_action_server() - def load_ftrack_url(self): - ftrack_url = ( - get_system_settings() - ["modules"] - ["Ftrack"] - ["ftrack_server"] - ) - os.environ["FTRACK_SERVER"] = ftrack_url - # Definition of visibility of each menu actions def set_menu_visibility(self): self.tray_server_menu.menuAction().setVisible(self.bool_logged) @@ -348,18 +339,6 @@ class FtrackModule: credentials.set_env() self.validate() - def process_modules(self, modules): - if 'TimersManager' in modules: - self.timer_manager = modules['TimersManager'] - self.timer_manager.add_module(self) - - if "UserModule" in modules: - credentials.USER_GETTER = modules["UserModule"].get_user - modules["UserModule"].register_callback_on_user_change( - self.changed_user - ) - - def start_timer_manager(self, data): if self.thread_timer is not None: self.thread_timer.ftrack_start_timer(data) @@ -369,12 +348,10 @@ class FtrackModule: self.thread_timer.ftrack_stop_timer() def timer_started(self, data): - if hasattr(self, 'timer_manager'): - self.timer_manager.start_timers(data) + self.module.timer_started(data) def timer_stopped(self): - if hasattr(self, 'timer_manager'): - self.timer_manager.stop_timers() + self.module.timer_stopped() class FtrackEventsThread(QtCore.QThread): diff --git a/pype/modules/ftrack/tray/login_dialog.py b/pype/modules/ftrack/tray/login_dialog.py index 6c7373e337..a49010effc 100644 --- a/pype/modules/ftrack/tray/login_dialog.py +++ b/pype/modules/ftrack/tray/login_dialog.py @@ -3,7 +3,7 @@ import requests from avalon import style from pype.modules.ftrack.lib import credentials from . import login_tools -from pype.api import resources +from pype import resources from Qt import QtCore, QtGui, QtWidgets diff --git a/pype/modules/ftrack/tray/login_tools.py b/pype/modules/ftrack/tray/login_tools.py index d3297eaa76..328ce49f5c 100644 --- a/pype/modules/ftrack/tray/login_tools.py +++ b/pype/modules/ftrack/tray/login_tools.py @@ -3,7 +3,7 @@ from urllib import parse import webbrowser import functools import threading -from pype.api import resources +from pype import resources class LoginServerHandler(BaseHTTPRequestHandler): diff --git a/pype/modules/idle_manager/__init__.py b/pype/modules/idle_manager/__init__.py index f1a87bef41..4bc33c87c1 100644 --- a/pype/modules/idle_manager/__init__.py +++ b/pype/modules/idle_manager/__init__.py @@ -1,5 +1,10 @@ -from .idle_manager import IdleManager +from .idle_manager import ( + IdleManager, + IIdleManager +) -def tray_init(tray_widget, main_widget): - return IdleManager() +__all__ = ( + "IdleManager", + "IIdleManager" +) diff --git a/pype/modules/idle_manager/idle_manager.py b/pype/modules/idle_manager/idle_manager.py index 3a9f9154a9..92592f6add 100644 --- a/pype/modules/idle_manager/idle_manager.py +++ b/pype/modules/idle_manager/idle_manager.py @@ -1,65 +1,131 @@ import time import collections import threading +from abc import ABCMeta, abstractmethod + +import six from pynput import mouse, keyboard -from pype.api import Logger + +from pype.lib import PypeLogger +from pype.modules import PypeModule, ITrayService -class IdleManager(threading.Thread): +@six.add_metaclass(ABCMeta) +class IIdleManager: + """Other modules interface to return callbacks by idle time in seconds. + + Expected output is dictionary with seconds as keys and callback/s + as value, value may be callback of list of callbacks. + EXAMPLE: + ``` + { + 60: self.on_minute_idle + } + ``` + """ + idle_manager = None + + @abstractmethod + def callbacks_by_idle_time(self): + pass + + @property + def idle_time(self): + if self.idle_manager: + return self.idle_manager.idle_time + + +class IdleManager(PypeModule, ITrayService): """ Measure user's idle time in seconds. Idle time resets on keyboard/mouse input. Is able to emit signals at specific time idle. """ - time_callbacks = collections.defaultdict(list) - idle_time = 0 + label = "Idle Service" + name = "Idle Manager" - def __init__(self): - super(IdleManager, self).__init__() - self.log = Logger().get_logger(self.__class__.__name__) - self.qaction = None - self.failed_icon = None - self._is_running = False - self.threads = [] + def initialize(self, module_settings): + idle_man_settings = module_settings[self.name] + self.enabled = idle_man_settings["enabled"] - def set_qaction(self, qaction, failed_icon): - self.qaction = qaction - self.failed_icon = failed_icon + self.time_callbacks = collections.defaultdict(list) + self.idle_thread = None + + def tray_init(self): + return def tray_start(self): - self.start() + self.start_thread() def tray_exit(self): - self.stop() + self.stop_thread() try: self.time_callbacks = {} except Exception: pass - def add_time_callback(self, emit_time, callback): - """If any module want to use IdleManager, need to use this method. + def connect_with_modules(self, enabled_modules): + for module in enabled_modules: + if not isinstance(module, IIdleManager): + continue - Args: - emit_time(int): Time when callback will be triggered. - callback(func): Callback that will be triggered. - """ - self.time_callbacks[emit_time].append(callback) + module.idle_manager = self + callbacks_items = module.callbacks_by_idle_time() or {} + for emit_time, callbacks in callbacks_items.items(): + if not isinstance(callbacks, (tuple, list, set)): + callbacks = [callbacks] + self.time_callbacks[emit_time].extend(callbacks) @property - def is_running(self): - return self._is_running + def idle_time(self): + if self.idle_thread and self.idle_thread.is_running: + return self.idle_thread.idle_time - def _reset_time(self): + def start_thread(self): + if self.idle_thread: + self.idle_thread.stop() + self.idle_thread.join() + self.idle_thread = IdleManagerThread(self) + self.idle_thread.start() + + def stop_thread(self): + if self.idle_thread: + self.idle_thread.stop() + self.idle_thread.join() + + def on_thread_stop(self): + self.set_service_failed_icon() + + +class IdleManagerThread(threading.Thread): + def __init__(self, module, *args, **kwargs): + super(IdleManagerThread, self).__init__(*args, **kwargs) + self.log = PypeLogger().get_logger(self.__class__.__name__) + self.module = module + self.threads = [] + self.is_running = False self.idle_time = 0 def stop(self): - self._is_running = False + self.is_running = False + + def reset_time(self): + self.idle_time = 0 + + @property + def time_callbacks(self): + return self.module.time_callbacks + + def on_stop(self): + self.is_running = False + self.log.info("IdleManagerThread has stopped") + self.module.on_thread_stop() def run(self): - self.log.info('IdleManager has started') - self._is_running = True - thread_mouse = MouseThread(self._reset_time) + self.log.info("IdleManagerThread has started") + self.is_running = True + thread_mouse = MouseThread(self.reset_time) thread_mouse.start() - thread_keyboard = KeyboardThread(self._reset_time) + thread_keyboard = KeyboardThread(self.reset_time) thread_keyboard.start() try: while self.is_running: @@ -82,9 +148,6 @@ class IdleManager(threading.Thread): 'Idle Manager service has failed', exc_info=True ) - if self.qaction and self.failed_icon: - self.qaction.setIcon(self.failed_icon) - # Threads don't have their attrs when Qt application already finished try: thread_mouse.stop() @@ -98,8 +161,7 @@ class IdleManager(threading.Thread): except AttributeError: pass - self._is_running = False - self.log.info('IdleManager has stopped') + self.on_stop() class MouseThread(mouse.Listener): diff --git a/pype/modules/logging/__init__.py b/pype/modules/logging/__init__.py new file mode 100644 index 0000000000..c87d8b7f43 --- /dev/null +++ b/pype/modules/logging/__init__.py @@ -0,0 +1,6 @@ +from .logging_module import LoggingModule + + +__all__ = ( + "LoggingModule", +) diff --git a/pype/modules/logging/tray/logging_module.py b/pype/modules/logging/logging_module.py similarity index 50% rename from pype/modules/logging/tray/logging_module.py rename to pype/modules/logging/logging_module.py index 84b40f68e1..2aa13cc118 100644 --- a/pype/modules/logging/tray/logging_module.py +++ b/pype/modules/logging/logging_module.py @@ -1,41 +1,46 @@ from pype.api import Logger +from .. import PypeModule, ITrayModule -class LoggingModule: - def __init__(self, main_parent=None, parent=None): - self.parent = parent - self.log = Logger().get_logger(self.__class__.__name__, "logging") +class LoggingModule(PypeModule, ITrayModule): + name = "Logging" + def initialize(self, modules_settings): + logging_settings = modules_settings[self.name] + self.enabled = logging_settings["enabled"] + + # Tray attributes self.window = None - self.tray_init(main_parent, parent) - - def tray_init(self, main_parent, parent): + def tray_init(self): try: - from .gui.app import LogsWindow + from .tray.app import LogsWindow self.window = LogsWindow() - self.tray_menu = self._tray_menu except Exception: self.log.warning( "Couldn't set Logging GUI due to error.", exc_info=True ) # Definition of Tray menu - def _tray_menu(self, parent_menu): + def tray_menu(self, tray_menu): from Qt import QtWidgets # Menu for Tray App - menu = QtWidgets.QMenu('Logging', parent_menu) + menu = QtWidgets.QMenu('Logging', tray_menu) show_action = QtWidgets.QAction("Show Logs", menu) show_action.triggered.connect(self._show_logs_gui) menu.addAction(show_action) - parent_menu.addMenu(menu) + tray_menu.addMenu(menu) def tray_start(self): - pass + return - def process_modules(self, modules): + def tray_exit(self): + return + + def connect_with_modules(self, _enabled_modules): + """Nothing special.""" return def _show_logs_gui(self): diff --git a/pype/modules/logging/tray/__init__.py b/pype/modules/logging/tray/__init__.py index a2586155e7..e69de29bb2 100644 --- a/pype/modules/logging/tray/__init__.py +++ b/pype/modules/logging/tray/__init__.py @@ -1,5 +0,0 @@ -from .logging_module import LoggingModule - - -def tray_init(tray_widget, main_widget): - return LoggingModule(main_widget, tray_widget) diff --git a/pype/modules/logging/tray/gui/app.py b/pype/modules/logging/tray/app.py similarity index 100% rename from pype/modules/logging/tray/gui/app.py rename to pype/modules/logging/tray/app.py diff --git a/pype/modules/logging/tray/gui/__init__.py b/pype/modules/logging/tray/gui/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/pype/modules/logging/tray/gui/models.py b/pype/modules/logging/tray/models.py similarity index 100% rename from pype/modules/logging/tray/gui/models.py rename to pype/modules/logging/tray/models.py diff --git a/pype/modules/logging/tray/gui/widgets.py b/pype/modules/logging/tray/widgets.py similarity index 100% rename from pype/modules/logging/tray/gui/widgets.py rename to pype/modules/logging/tray/widgets.py diff --git a/pype/modules/muster/__init__.py b/pype/modules/muster/__init__.py index 9429cbe561..d194f8f3c2 100644 --- a/pype/modules/muster/__init__.py +++ b/pype/modules/muster/__init__.py @@ -1,5 +1,6 @@ from .muster import MusterModule -def tray_init(tray_widget, main_widget): - return MusterModule(main_widget, tray_widget) +__all__ = ( + "MusterModule", +) diff --git a/pype/modules/muster/muster.py b/pype/modules/muster/muster.py index beb30690ac..393a10e29a 100644 --- a/pype/modules/muster/muster.py +++ b/pype/modules/muster/muster.py @@ -2,9 +2,10 @@ import os import json import appdirs import requests +from .. import PypeModule, ITrayModule, IRestApi -class MusterModule: +class MusterModule(PypeModule, ITrayModule, IRestApi): """ Module handling Muster Render credentials. This will display dialog asking for user credentials for Muster if not already specified. @@ -14,38 +15,42 @@ class MusterModule: ) cred_filename = 'muster_cred.json' - def __init__(self, main_parent=None, parent=None): + name = "muster" + + def initialize(self, modules_settings): + muster_settings = modules_settings[self.name] + self.enabled = muster_settings["enabled"] + self.muster_url = muster_settings["MUSTER_REST_URL"] + self.cred_path = os.path.join( self.cred_folder_path, self.cred_filename ) - self.tray_init(main_parent, parent) + # Tray attributes + self.widget_login = None + self.action_show_login = None - def tray_init(self, main_parent, parent): + def get_global_environments(self): + return { + "MUSTER_REST_URL": self.muster_url + } + + def tray_init(self): from .widget_login import MusterLogin - - self.main_parent = main_parent - self.parent = parent - self.widget_login = MusterLogin(main_parent, self) + self.widget_login = MusterLogin(self) def tray_start(self): - """ - Show login dialog if credentials not found. - """ + """Show login dialog if credentials not found.""" # This should be start of module in tray cred = self.load_credentials() if not cred: self.show_login() - else: - # nothing to do - pass - def process_modules(self, modules): - if "RestApiServer" in modules: - def api_show_login(): - self.aShowLogin.trigger() - modules["RestApiServer"].register_callback( - "/show_login", api_show_login, "muster", "post" - ) + def tray_exit(self): + """Nothing special for Muster.""" + return + + def connect_with_modules(self, *_a, **_kw): + return # Definition of Tray menu def tray_menu(self, parent): @@ -53,18 +58,26 @@ class MusterModule: from Qt import QtWidgets # Menu for Tray App - self.menu = QtWidgets.QMenu('Muster', parent) - self.menu.setProperty('submenu', 'on') + menu = QtWidgets.QMenu('Muster', parent) + menu.setProperty('submenu', 'on') # Actions - self.aShowLogin = QtWidgets.QAction( - "Change login", self.menu + self.action_show_login = QtWidgets.QAction( + "Change login", menu ) - self.menu.addAction(self.aShowLogin) - self.aShowLogin.triggered.connect(self.show_login) + menu.addAction(self.action_show_login) + self.action_show_login.triggered.connect(self.show_login) - parent.addMenu(self.menu) + parent.addMenu(menu) + + def rest_api_initialization(self, rest_api_module): + """Implementation of IRestApi interface.""" + def api_show_login(): + self.action_show_login.trigger() + rest_api_module.register_callback( + "/show_login", api_show_login, "muster", "post" + ) def load_credentials(self): """ @@ -84,8 +97,7 @@ class MusterModule: """ Authenticate user with Muster and get authToken from server. """ - MUSTER_REST_URL = os.environ.get("MUSTER_REST_URL") - if not MUSTER_REST_URL: + if not self.muster_url: raise AttributeError("Muster REST API url not set") params = { 'username': username, @@ -93,7 +105,7 @@ class MusterModule: } api_entry = '/api/login' response = self._requests_post( - MUSTER_REST_URL + api_entry, params=params) + self.muster_url + api_entry, params=params) if response.status_code != 200: self.log.error( 'Cannot log into Muster: {}'.format(response.status_code)) @@ -123,7 +135,8 @@ class MusterModule: """ Show dialog to enter credentials """ - self.widget_login.show() + if self.widget_login: + self.widget_login.show() def _requests_post(self, *args, **kwargs): """ Wrapper for requests, disabling SSL certificate validation if diff --git a/pype/modules/muster/widget_login.py b/pype/modules/muster/widget_login.py index f446c13325..0fd1913d0c 100644 --- a/pype/modules/muster/widget_login.py +++ b/pype/modules/muster/widget_login.py @@ -1,7 +1,7 @@ import os from Qt import QtCore, QtGui, QtWidgets from avalon import style -from pype.api import resources +from pype import resources class MusterLogin(QtWidgets.QWidget): @@ -11,21 +11,15 @@ class MusterLogin(QtWidgets.QWidget): loginSignal = QtCore.Signal(object, object, object) - def __init__(self, main_parent=None, parent=None): + def __init__(self, module, parent=None): - super(MusterLogin, self).__init__() + super(MusterLogin, self).__init__(parent) - self.parent_widget = parent - self.main_parent = main_parent + self.module = module # Icon - if hasattr(parent, 'icon'): - self.setWindowIcon(parent.icon) - elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'): - self.setWindowIcon(parent.parent.icon) - else: - icon = QtGui.QIcon(resources.pype_icon_filepath()) - self.setWindowIcon(icon) + icon = QtGui.QIcon(resources.pype_icon_filepath()) + self.setWindowIcon(icon) self.setWindowFlags( QtCore.Qt.WindowCloseButtonHint | @@ -153,7 +147,7 @@ class MusterLogin(QtWidgets.QWidget): self._close_widget() def save_credentials(self, username, password): - self.parent_widget.get_auth_token(username, password) + self.module.get_auth_token(username, password) def closeEvent(self, event): event.ignore() diff --git a/pype/modules/rest_api/__init__.py b/pype/modules/rest_api/__init__.py index 55253bc58b..b3312e8d31 100644 --- a/pype/modules/rest_api/__init__.py +++ b/pype/modules/rest_api/__init__.py @@ -1,9 +1,27 @@ -from .rest_api import RestApiServer -from .base_class import RestApi, abort, route, register_statics -from .lib import RestMethods, CallbackResult +from .rest_api import ( + RestApiModule, + IRestApi +) +from .base_class import ( + RestApi, + abort, + route, + register_statics +) +from .lib import ( + RestMethods, + CallbackResult +) -CLASS_DEFINIION = RestApiServer +__all__ = ( + "RestApiModule", + "IRestApi", + "RestApi", + "abort", + "route", + "register_statics", -def tray_init(tray_widget, main_widget): - return RestApiServer() + "RestMethods", + "CallbackResult" +) diff --git a/pype/modules/rest_api/rest_api.py b/pype/modules/rest_api/rest_api.py index 3e0c646560..bd9e7d2a83 100644 --- a/pype/modules/rest_api/rest_api.py +++ b/pype/modules/rest_api/rest_api.py @@ -1,21 +1,32 @@ import os import socket import threading - +from abc import ABCMeta, abstractmethod from socketserver import ThreadingMixIn from http.server import HTTPServer + +import six + +from pype.lib import PypeLogger +from pype import resources + from .lib import RestApiFactory, Handler from .base_class import route, register_statics -from pype.api import Logger - -log = Logger().get_logger("RestApiServer") +from .. import PypeModule, ITrayService -class ThreadingSimpleServer(ThreadingMixIn, HTTPServer): - pass +@six.add_metaclass(ABCMeta) +class IRestApi: + """Other modules interface to return paths to ftrack event handlers. + + Expected output is dictionary with "server" and "user" keys. + """ + @abstractmethod + def rest_api_initialization(self, rest_api_module): + pass -class RestApiServer: +class RestApiModule(PypeModule, ITrayService): """Rest Api allows to access statics or callbacks with http requests. To register statics use `register_statics`. @@ -85,30 +96,18 @@ class RestApiServer: Callback may return many types. For more information read docstring of `_handle_callback_result` defined in handler. """ - default_port = 8011 - exclude_ports = [] + label = "Rest API Service" + name = "Rest Api" - def __init__(self): - self.qaction = None - self.failed_icon = None - self._is_running = False + def initialize(self, modules_settings): + rest_api_settings = modules_settings[self.name] + self.enabled = True + self.default_port = rest_api_settings["default_port"] + self.exclude_ports = rest_api_settings["exclude_ports"] - port = self.find_port() - self.rest_api_thread = RestApiThread(self, port) - - statics_dir = os.path.join( - os.environ["PYPE_MODULE_ROOT"], - "pype", - "resources" - ) - self.register_statics("/res", statics_dir) - os.environ["PYPE_STATICS_SERVER"] = "{}/res".format( - os.environ["PYPE_REST_API_URL"] - ) - - def set_qaction(self, qaction, failed_icon): - self.qaction = qaction - self.failed_icon = failed_icon + self.rest_api_url = None + self.rest_api_thread = None + self.resources_url = None def register_callback( self, path, callback, url_prefix="", methods=[], strict_match=False @@ -123,6 +122,15 @@ class RestApiServer: def register_obj(self, obj): RestApiFactory.register_obj(obj) + def connect_with_modules(self, enabled_modules): + # Do not register restapi callbacks out of tray + if self.tray_initialized: + for module in enabled_modules: + if not isinstance(module, IRestApi): + continue + + module.rest_api_initialization(self) + def find_port(self): start_port = self.default_port exclude_ports = self.exclude_ports @@ -139,15 +147,23 @@ class RestApiServer: break if found_port is None: return None - os.environ["PYPE_REST_API_URL"] = "http://localhost:{}".format( - found_port - ) return found_port + def tray_init(self): + port = self.find_port() + self.rest_api_url = "http://localhost:{}".format(port) + self.rest_api_thread = RestApiThread(self, port) + self.register_statics("/res", resources.RESOURCES_DIR) + self.resources_url = "{}/res".format(self.rest_api_url) + + # Set rest api environments + os.environ["PYPE_REST_API_URL"] = self.rest_api_url + os.environ["PYPE_STATICS_SERVER"] = self.resources_url + def tray_start(self): RestApiFactory.prepare_registered() if not RestApiFactory.has_handlers(): - log.debug("There are not registered any handlers for RestApi") + self.log.debug("There are not registered any handlers for RestApi") return self.rest_api_thread.start() @@ -163,6 +179,10 @@ class RestApiServer: self.rest_api_thread.join() +class ThreadingSimpleServer(ThreadingMixIn, HTTPServer): + pass + + class RestApiThread(threading.Thread): """ Listener for REST requests. @@ -176,6 +196,7 @@ class RestApiThread(threading.Thread): self.module = module self.port = port self.httpd = None + self.log = PypeLogger().get_logger("RestApiThread") def stop(self): self.is_running = False @@ -186,7 +207,7 @@ class RestApiThread(threading.Thread): self.is_running = True try: - log.debug( + self.log.debug( "Running Rest Api server on URL:" " \"http://localhost:{}\"".format(self.port) ) @@ -197,7 +218,7 @@ class RestApiThread(threading.Thread): httpd.handle_request() except Exception: - log.warning( + self.log.warning( "Rest Api Server service has failed", exc_info=True ) diff --git a/pype/modules/standalonepublish/__init__.py b/pype/modules/standalonepublish/__init__.py index 4038b696d9..5c40deb6f0 100644 --- a/pype/modules/standalonepublish/__init__.py +++ b/pype/modules/standalonepublish/__init__.py @@ -1,5 +1,5 @@ from .standalonepublish_module import StandAlonePublishModule - -def tray_init(tray_widget, main_widget): - return StandAlonePublishModule(main_widget, tray_widget) +__all__ = ( + "StandAlonePublishModule", +) diff --git a/pype/modules/standalonepublish/standalonepublish_module.py b/pype/modules/standalonepublish/standalonepublish_module.py index f8bc0c6f24..abed6bddd9 100644 --- a/pype/modules/standalonepublish/standalonepublish_module.py +++ b/pype/modules/standalonepublish/standalonepublish_module.py @@ -2,36 +2,45 @@ import os import sys import subprocess import pype +from .. import PypeModule, ITrayModule -class StandAlonePublishModule: - def __init__(self, main_parent=None, parent=None): - self.main_parent = main_parent - self.parent_widget = parent +class StandAlonePublishModule(PypeModule, ITrayModule): + menu_label = "Publish" + name = "Standalone Publish" + + def initialize(self, modules_settings): + self.enabled = modules_settings[self.name]["enabled"] self.publish_paths = [ os.path.join( pype.PLUGINS_DIR, "standalonepublisher", "publish" ) ] + def tray_init(self): + return + + def tray_start(self): + return + + def tray_exit(self): + return + def tray_menu(self, parent_menu): from Qt import QtWidgets - self.run_action = QtWidgets.QAction( - "Publish", parent_menu - ) - self.run_action.triggered.connect(self.show) - parent_menu.addAction(self.run_action) + run_action = QtWidgets.QAction(self.menu_label, parent_menu) + run_action.triggered.connect(self.run_standalone_publisher) + parent_menu.addAction(run_action) - def process_modules(self, modules): - if "FtrackModule" in modules: - self.publish_paths.append(os.path.join( - pype.PLUGINS_DIR, "ftrack", "publish" - )) + def connect_with_modules(self, enabled_modules): + """Collect publish paths from other modules.""" + publish_paths = self.manager.collect_plugin_paths()["publish"] + self.publish_paths.extend(publish_paths) - def show(self): + def run_standalone_publisher(self): from pype import tools standalone_publisher_tool_path = os.path.join( - os.path.dirname(tools.__file__), + os.path.dirname(os.path.abspath(tools.__file__)), "standalonepublish" ) subprocess.Popen([ diff --git a/pype/modules/timers_manager/__init__.py b/pype/modules/timers_manager/__init__.py index 9de205f088..1b565cc59a 100644 --- a/pype/modules/timers_manager/__init__.py +++ b/pype/modules/timers_manager/__init__.py @@ -1,7 +1,9 @@ -from .timers_manager import TimersManager +from .timers_manager import ( + ITimersManager, + TimersManager +) -CLASS_DEFINIION = TimersManager - - -def tray_init(tray_widget, main_widget): - return TimersManager(tray_widget, main_widget) +__all__ = ( + "ITimersManager", + "TimersManager" +) diff --git a/pype/modules/timers_manager/timers_manager.py b/pype/modules/timers_manager/timers_manager.py index 62767c24f1..f4f5243f13 100644 --- a/pype/modules/timers_manager/timers_manager.py +++ b/pype/modules/timers_manager/timers_manager.py @@ -1,118 +1,137 @@ -from pype.api import Logger +from abc import ABCMeta, abstractmethod +import six +from .. import PypeModule, ITrayService, IIdleManager -class TimersManager: +@six.add_metaclass(ABCMeta) +class ITimersManager: + timer_manager_module = None + + @abstractmethod + def stop_timer(self): + pass + + @abstractmethod + def start_timer(self, data): + pass + + def timer_started(self, data): + if not self.timer_manager_module: + return + + self.timer_manager_module.timer_started(self.id, data) + + def timer_stopped(self): + if not self.timer_manager_module: + return + + self.timer_manager_module.timer_stopped(self.id) + + +class TimersManager(PypeModule, ITrayService, IIdleManager): """ Handles about Timers. Should be able to start/stop all timers at once. 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" + label = "Timers Service" - # Presetable attributes - # - when timer will stop if idle manager is running (minutes) - full_time = 15 - # - how many minutes before the timer is stopped will popup the message - message_time = 0.5 + def initialize(self, modules_settings): + timers_settings = modules_settings[self.name] - def __init__(self, tray_widget, main_widget): - self.log = Logger().get_logger(self.__class__.__name__) + self.enabled = timers_settings["enabled"] + # When timer will stop if idle manager is running (minutes) + full_time = int(timers_settings["full_time"] * 60) + # How many minutes before the timer is stopped will popup the message + message_time = int(timers_settings["message_time"] * 60) + + self.time_show_message = full_time - message_time + self.time_stop_timer = full_time - self.modules = [] self.is_running = False self.last_task = None - self.tray_widget = tray_widget - self.main_widget = main_widget - - self.idle_man = None + # Tray attributes + self.signal_handler = None + self.widget_user_idle = None self.signal_handler = None - self.trat_init(tray_widget, main_widget) + self.modules = [] - def trat_init(self, tray_widget, main_widget): + def tray_init(self): from .widget_user_idle import WidgetUserIdle, SignalHandler - self.widget_user_idle = WidgetUserIdle(self, tray_widget) + self.widget_user_idle = WidgetUserIdle(self) self.signal_handler = SignalHandler(self) - def set_signal_times(self): - try: - full_time = int(self.full_time * 60) - message_time = int(self.message_time * 60) - self.time_show_message = full_time - message_time - self.time_stop_timer = full_time - return True - except Exception: - self.log.error("Couldn't set timer signals.", exc_info=True) + def tray_start(self, *_a, **_kw): + return - def add_module(self, module): - """ Adds module to context + def tray_exit(self): + """Nothing special for TimersManager.""" + return - Module must have implemented methods: - - ``start_timer_manager(data)`` - - ``stop_timer_manager()`` - """ - self.modules.append(module) - - def start_timers(self, data): - ''' - :param data: basic information needed to start any timer - :type data: dict - ..note:: - Dictionary "data" should contain: - - project_name(str) - Name of Project - - hierarchy(list/tuple) - list of parents(except project) - - task_type(str) - - task_name(str) - - Example: - - to run timers for task in - 'C001_BackToPast/assets/characters/villian/Lookdev BG' - - input data should contain: - .. code-block:: Python - data = { - 'project_name': 'C001_BackToPast', - 'hierarchy': ['assets', 'characters', 'villian'], - 'task_type': 'lookdev', - 'task_name': 'Lookdev BG' - } - ''' - if len(data['hierarchy']) < 1: - self.log.error(( - 'Not allowed action in Pype!!' - ' Timer has been launched on task which is child of Project.' - )) - return + def timer_started(self, source_id, data): + for module in self.modules: + if module.id != source_id: + module.start_timer(data) self.last_task = data - - for module in self.modules: - module.start_timer_manager(data) self.is_running = True + def timer_stopped(self, source_id): + for module in self.modules: + if module.id != source_id: + module.stop_timer() + def restart_timers(self): if self.last_task is not None: - self.start_timers(self.last_task) + self.timer_started(None, self.last_task) def stop_timers(self): if self.is_running is False: return + self.widget_user_idle.bool_not_stopped = False self.widget_user_idle.refresh_context() - for module in self.modules: - module.stop_timer_manager() self.is_running = False - def process_modules(self, modules): - """ Gives ability to connect with imported modules from TrayManager. + for module in self.modules: + module.stop_timer() - :param modules: All imported modules from TrayManager - :type modules: dict - """ + def connect_with_modules(self, enabled_modules): + for module in enabled_modules: + if not isinstance(module, ITimersManager): + continue + module.timer_manager_module = self + self.modules.append(module) - if 'IdleManager' in modules: - if self.set_signal_times() is True: - self.register_to_idle_manager(modules['IdleManager']) + def callbacks_by_idle_time(self): + """Implementation of IIdleManager interface.""" + # Time when message is shown + callbacks = { + self.time_show_message: lambda: self.time_callback(0) + } + + # Times when idle is between show widget and stop timers + show_to_stop_range = range( + self.time_show_message - 1, self.time_stop_timer + ) + for num in show_to_stop_range: + callbacks[num] = lambda: self.time_callback(1) + + # Times when widget is already shown and user restart idle + shown_and_moved_range = range( + self.time_stop_timer - self.time_show_message + ) + for num in shown_and_moved_range: + callbacks[num] = lambda: self.time_callback(1) + + # Time when timers are stopped + callbacks[self.time_stop_timer] = lambda: self.time_callback(2) + + return callbacks def time_callback(self, int_def): if not self.signal_handler: @@ -125,51 +144,23 @@ class TimersManager: elif int_def == 2: self.signal_handler.signal_stop_timers.emit() - def register_to_idle_manager(self, man_obj): - self.idle_man = man_obj - - # Time when message is shown - self.idle_man.add_time_callback( - self.time_show_message, - lambda: self.time_callback(0) - ) - - # Times when idle is between show widget and stop timers - show_to_stop_range = range( - self.time_show_message - 1, self.time_stop_timer - ) - for num in show_to_stop_range: - self.idle_man.add_time_callback( - num, lambda: self.time_callback(1) - ) - # Times when widget is already shown and user restart idle - shown_and_moved_range = range( - self.time_stop_timer - self.time_show_message - ) - for num in shown_and_moved_range: - self.idle_man.add_time_callback( - num, lambda: self.time_callback(1) - ) - - # Time when timers are stopped - self.idle_man.add_time_callback( - self.time_stop_timer, - lambda: self.time_callback(2) - ) - def change_label(self): if self.is_running is False: return - if not self.idle_man or self.widget_user_idle.bool_is_showed is False: + + if ( + not self.idle_manager + or self.widget_user_idle.bool_is_showed is False + ): return - if self.idle_man.idle_time > self.time_show_message: - value = self.time_stop_timer - self.idle_man.idle_time + if self.idle_manager.idle_time > self.time_show_message: + value = self.time_stop_timer - self.idle_manager.idle_time else: value = 1 + ( self.time_stop_timer - self.time_show_message - - self.idle_man.idle_time + self.idle_manager.idle_time ) self.widget_user_idle.change_count_widget(value) diff --git a/pype/modules/timers_manager/widget_user_idle.py b/pype/modules/timers_manager/widget_user_idle.py index 22455846fd..5e47cdaddf 100644 --- a/pype/modules/timers_manager/widget_user_idle.py +++ b/pype/modules/timers_manager/widget_user_idle.py @@ -1,5 +1,6 @@ from avalon import style from Qt import QtCore, QtGui, QtWidgets +from pype import resources class WidgetUserIdle(QtWidgets.QWidget): @@ -7,7 +8,7 @@ class WidgetUserIdle(QtWidgets.QWidget): SIZE_W = 300 SIZE_H = 160 - def __init__(self, module, tray_widget): + def __init__(self, module): super(WidgetUserIdle, self).__init__() @@ -15,7 +16,9 @@ class WidgetUserIdle(QtWidgets.QWidget): self.bool_not_stopped = True self.module = module - self.setWindowIcon(tray_widget.icon) + + icon = QtGui.QIcon(resources.pype_icon_filepath()) + self.setWindowIcon(icon) self.setWindowFlags( QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowMinimizeButtonHint diff --git a/pype/modules/user/__init__.py b/pype/modules/user/__init__.py index 04fe392c2c..a97ac0eef6 100644 --- a/pype/modules/user/__init__.py +++ b/pype/modules/user/__init__.py @@ -1,5 +1,10 @@ -from .user_module import UserModule +from .user_module import ( + UserModule, + IUserModule +) -def tray_init(tray_widget, main_widget): - return UserModule(main_widget, tray_widget) +__all__ = ( + "UserModule", + "IUserModule" +) diff --git a/pype/modules/user/user_module.py b/pype/modules/user/user_module.py index dc57fe4a63..240ede947b 100644 --- a/pype/modules/user/user_module.py +++ b/pype/modules/user/user_module.py @@ -2,38 +2,55 @@ import os import json import getpass +from abc import ABCMeta, abstractmethod + +import six import appdirs -from pype.api import Logger +from .. import PypeModule, ITrayModule, IRestApi -class UserModule: +@six.add_metaclass(ABCMeta) +class IUserModule: + """Interface for other modules to use user change callbacks.""" + + @abstractmethod + def on_pype_user_change(self, username): + """What should happen on Pype user change.""" + pass + + +class UserModule(PypeModule, ITrayModule, IRestApi): cred_folder_path = os.path.normpath( appdirs.user_data_dir('pype-app', 'pype') ) cred_filename = 'user_info.json' env_name = "PYPE_USERNAME" - log = Logger().get_logger("UserModule", "user") + name = "User setting" - def __init__(self, main_parent=None, parent=None): - self._callbacks_on_user_change = [] + def initialize(self, modules_settings): + user_settings = modules_settings[self.name] + self.enabled = user_settings["enabled"] + + self.callbacks_on_user_change = [] self.cred = {} self.cred_path = os.path.normpath(os.path.join( self.cred_folder_path, self.cred_filename )) + + # Tray attributes self.widget_login = None + self.action_show_widget = None - self.tray_init(main_parent, parent) - - def tray_init(self, main_parent=None, parent=None): + def tray_init(self): from .widget_user import UserWidget self.widget_login = UserWidget(self) self.load_credentials() def register_callback_on_user_change(self, callback): - self._callbacks_on_user_change.append(callback) + self.callbacks_on_user_change.append(callback) def tray_start(self): """Store credentials to env and preset them to widget""" @@ -44,29 +61,34 @@ class UserModule: os.environ[self.env_name] = username self.widget_login.set_user(username) + def tray_exit(self): + """Nothing special for User.""" + return + def get_user(self): return self.cred.get("username") or getpass.getuser() - def process_modules(self, modules): - """ Gives ability to connect with imported modules from TrayManager. + def rest_api_initialization(self, rest_api_module): + def api_get_username(): + return self.cred - :param modules: All imported modules from TrayManager - :type modules: dict - """ + rest_api_module.register_callback( + "user/username", api_get_username, "get" + ) - if "RestApiServer" in modules: - def api_get_username(): - return self.cred + def api_show_widget(): + self.action_show_widget.trigger() - def api_show_widget(): - self.action_show_widget.trigger() + rest_api_module.register_callback( + "user/show_widget", api_show_widget, "post" + ) - modules["RestApiServer"].register_callback( - "user/username", api_get_username, "get" - ) - modules["RestApiServer"].register_callback( - "user/show_widget", api_show_widget, "post" - ) + def connect_with_modules(self, enabled_modules): + for module in enabled_modules: + if isinstance(module, IUserModule): + self.callbacks_on_user_change.append( + module.on_pype_user_change + ) # Definition of Tray menu def tray_menu(self, parent_menu): @@ -108,12 +130,14 @@ class UserModule: def change_credentials(self, username): self.save_credentials(username) - for callback in self._callbacks_on_user_change: + for callback in self.callbacks_on_user_change: try: - callback() + callback(username) except Exception: self.log.warning( - "Failed to execute callback \"{}\".".format(str(callback)), + "Failed to execute callback \"{}\".".format( + str(callback) + ), exc_info=True ) @@ -135,7 +159,9 @@ class UserModule: self.log.debug("Username \"{}\" stored".format(username)) except Exception: self.log.error( - "Could not store username to file \"{}\"".format(self.cred_path), + "Could not store username to file \"{}\"".format( + self.cred_path + ), exc_info=True ) diff --git a/pype/modules/user/widget_user.py b/pype/modules/user/widget_user.py index ba211c4737..d12cd6175c 100644 --- a/pype/modules/user/widget_user.py +++ b/pype/modules/user/widget_user.py @@ -1,6 +1,6 @@ from Qt import QtCore, QtGui, QtWidgets from avalon import style -from pype.api import resources +from pype import resources class UserWidget(QtWidgets.QWidget): diff --git a/pype/modules/websocket_server/__init__.py b/pype/modules/websocket_server/__init__.py index eb5a0d9f27..0f6888585f 100644 --- a/pype/modules/websocket_server/__init__.py +++ b/pype/modules/websocket_server/__init__.py @@ -1,5 +1,10 @@ -from .websocket_server import WebSocketServer +from .websocket_server import ( + WebsocketModule, + WebSocketServer +) -def tray_init(tray_widget, main_widget): - return WebSocketServer() +__all__ = ( + "WebsocketModule", + "WebSocketServer" +) diff --git a/pype/modules/websocket_server/websocket_server.py b/pype/modules/websocket_server/websocket_server.py index daf4b03103..7a6710349b 100644 --- a/pype/modules/websocket_server/websocket_server.py +++ b/pype/modules/websocket_server/websocket_server.py @@ -1,17 +1,53 @@ -from pype.api import Logger - -import threading -from aiohttp import web -import asyncio -from wsrpc_aiohttp import STATIC_DIR, WebSocketAsync - import os import sys import pyclbr import importlib import urllib +import threading -log = Logger().get_logger("WebsocketServer") +import six +from pype.lib import PypeLogger +from .. import PypeModule, ITrayService + +if six.PY2: + web = asyncio = STATIC_DIR = WebSocketAsync = None +else: + from aiohttp import web + import asyncio + from wsrpc_aiohttp import STATIC_DIR, WebSocketAsync + +log = PypeLogger().get_logger("WebsocketServer") + + +class WebsocketModule(PypeModule, ITrayService): + name = "Websocket server" + label = "Websocket server" + + def initialize(self, module_settings): + if asyncio is None: + raise AssertionError( + "WebSocketServer module requires Python 3.5 or higher." + ) + + self.enabled = True + self.websocket_server = None + + def connect_with_modules(self, *_a, **kw): + return + + def tray_init(self): + self.websocket_server = WebSocketServer() + self.websocket_server.on_stop_callbacks.append( + self.set_service_failed_icon + ) + + def tray_start(self): + if self.websocket_server: + self.websocket_server.module_start() + + def tray_exit(self): + if self.websocket_server: + self.websocket_server.module_stop() class WebSocketServer(): @@ -24,12 +60,11 @@ class WebSocketServer(): _instance = None def __init__(self): - self.qaction = None - self.failed_icon = None - self._is_running = False WebSocketServer._instance = self + self.client = None self.handlers = {} + self.on_stop_callbacks = [] port = None websocket_url = os.getenv("WEBSOCKET_URL") @@ -51,6 +86,14 @@ class WebSocketServer(): self.websocket_thread = WebsocketServerThread(self, port) + def module_start(self): + if self.websocket_thread: + self.websocket_thread.start() + + def module_stop(self): + if self.websocket_thread: + self.websocket_thread.stop() + def add_routes_for_directories(self, directories_with_routes): """ Loops through selected directories to find all modules and in them all classes implementing 'WebSocketRoute' that could be @@ -106,14 +149,7 @@ class WebSocketServer(): WebSocketServer() return WebSocketServer._instance - def tray_start(self): - self.websocket_thread.start() - - def tray_exit(self): - self.stop() - def stop_websocket_server(self): - self.stop() @property @@ -134,7 +170,8 @@ class WebSocketServer(): ) def thread_stopped(self): - self._is_running = False + for callback in self.on_stop_callbacks: + callback() class WebsocketServerThread(threading.Thread): @@ -145,7 +182,13 @@ class WebsocketServerThread(threading.Thread): it creates separate thread and separate asyncio event loop """ def __init__(self, module, port): + if asyncio is None: + raise AssertionError( + "WebSocketServer module requires Python 3.5 or higher." + ) + super(WebsocketServerThread, self).__init__() + self.is_running = False self.port = port self.module = module diff --git a/pype/modules_manager.py b/pype/modules_manager.py deleted file mode 100644 index 72023500e4..0000000000 --- a/pype/modules_manager.py +++ /dev/null @@ -1,102 +0,0 @@ -import os -import inspect - -import pype.modules -from pype.modules import PypeModule -from pype.settings import get_system_settings -from pype.api import Logger - - -class PypeModuleManager: - skip_module_names = ("__pycache__", ) - - def __init__(self): - self.log = Logger().get_logger( - "{}.{}".format(__name__, self.__class__.__name__) - ) - - self.pype_modules = self.find_pype_modules() - - def modules_environments(self): - environments = {} - for pype_module in self.pype_modules.values(): - environments.update(pype_module.startup_environments()) - return environments - - def find_pype_modules(self): - settings = get_system_settings() - modules = [] - dirpath = os.path.dirname(pype.modules.__file__) - for module_name in os.listdir(dirpath): - # Check if path lead to a folder - full_path = os.path.join(dirpath, module_name) - if not os.path.isdir(full_path): - continue - - # Skip known invalid names - if module_name in self.skip_module_names: - continue - - import_name = "pype.modules.{}".format(module_name) - try: - modules.append( - __import__(import_name, fromlist=[""]) - ) - - except Exception: - self.log.warning( - "Couldn't import {}".format(import_name), exc_info=True - ) - - pype_module_classes = [] - for module in modules: - try: - pype_module_classes.extend( - self._classes_from_module(PypeModule, module) - ) - except Exception: - self.log.warning( - "Couldn't import {}".format(import_name), exc_info=True - ) - - pype_modules = {} - for pype_module_class in pype_module_classes: - try: - pype_module = pype_module_class(settings) - if pype_module.enabled: - pype_modules[pype_module.id] = pype_module - except Exception: - self.log.warning( - "Couldn't create instance of {}".format( - pype_module_class.__class__.__name__ - ), - exc_info=True - ) - return pype_modules - - def _classes_from_module(self, superclass, module): - classes = list() - - def recursive_bases(klass): - output = [] - output.extend(klass.__bases__) - for base in klass.__bases__: - output.extend(recursive_bases(base)) - return output - - for name in dir(module): - # It could be anything at this point - obj = getattr(module, name) - - if not inspect.isclass(obj) or not len(obj.__bases__) > 0: - continue - - # Use string comparison rather than `issubclass` - # in order to support reloading of this module. - bases = recursive_bases(obj) - if not any(base.__name__ == superclass.__name__ for base in bases): - continue - - classes.append(obj) - - return classes diff --git a/pype/settings/defaults/system_settings/general.json b/pype/settings/defaults/system_settings/general.json index 4e0358f447..ff739cc75f 100644 --- a/pype/settings/defaults/system_settings/general.json +++ b/pype/settings/defaults/system_settings/general.json @@ -20,8 +20,7 @@ "PYPE_PROJECT_CONFIGS", "PYPE_PYTHON_EXE", "PYPE_OCIO_CONFIG", - "PYBLISH_GUI", - "PYBLISHPLUGINPATH" + "PYBLISH_GUI" ] }, "FFMPEG_PATH": { @@ -30,7 +29,6 @@ "linux": "{VIRTUAL_ENV}/localized/ffmpeg_exec/linux:{PYPE_SETUP_PATH}/vendor/bin/ffmpeg_exec/linux" }, "PATH": [ - "{PYPE_CONFIG}/launchers", "{FFMPEG_PATH}", "{PATH}" ], @@ -46,9 +44,6 @@ "darwin": "{VIRTUAL_ENV}/bin/python" }, "PYPE_OCIO_CONFIG": "{STUDIO_SOFT}/OpenColorIO-Configs", - "PYBLISH_GUI": "pyblish_pype", - "PYBLISHPLUGINPATH": [ - "{PYPE_MODULE_ROOT}/pype/plugins/ftrack/publish" - ] + "PYBLISH_GUI": "pyblish_pype" } } \ No newline at end of file diff --git a/pype/settings/defaults/system_settings/modules.json b/pype/settings/defaults/system_settings/modules.json index 558e078c5e..366edec8f0 100644 --- a/pype/settings/defaults/system_settings/modules.json +++ b/pype/settings/defaults/system_settings/modules.json @@ -1,42 +1,13 @@ { "Avalon": { - "AVALON_MONGO": "mongodb://localhost:2707", - "AVALON_DB_DATA": "{PYPE_SETUP_PATH}/../mongo_db_data", - "AVALON_THUMBNAIL_ROOT": "{PYPE_SETUP_PATH}/../avalon_thumails", - "environment": { - "__environment_keys__": { - "avalon": [ - "AVALON_CONFIG", - "AVALON_PROJECTS", - "AVALON_USERNAME", - "AVALON_PASSWORD", - "AVALON_DEBUG", - "AVALON_MONGO", - "AVALON_DB", - "AVALON_DB_DATA", - "AVALON_EARLY_ADOPTER", - "AVALON_SCHEMA", - "AVALON_LOCATION", - "AVALON_LABEL", - "AVALON_TIMEOUT", - "AVALON_THUMBNAIL_ROOT" - ] - }, - "AVALON_CONFIG": "pype", - "AVALON_PROJECTS": "{PYPE_PROJECTS_PATH}", - "AVALON_USERNAME": "avalon", - "AVALON_PASSWORD": "secret", - "AVALON_DEBUG": "1", - "AVALON_MONGO": "mongodb://localhost:2707", - "AVALON_DB": "avalon", - "AVALON_DB_DATA": "{PYPE_SETUP_PATH}/../mongo_db_data", - "AVALON_EARLY_ADOPTER": "1", - "AVALON_SCHEMA": "{PYPE_MODULE_ROOT}/schema", - "AVALON_LOCATION": "http://127.0.0.1", - "AVALON_LABEL": "Pype", - "AVALON_TIMEOUT": "1000", - "AVALON_THUMBNAIL_ROOT": "{PYPE_SETUP_PATH}/../avalon_thumails" - } + "AVALON_MONGO": "", + "AVALON_TIMEOUT": 1000, + "AVALON_THUMBNAIL_ROOT": { + "windows": "", + "darwin": "", + "linux": "" + }, + "AVALON_DB_DATA": "{PYPE_SETUP_PATH}/../mongo_db_data" }, "Ftrack": { "enabled": true, diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index 3e869f3e4a..c38e809c74 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -129,10 +129,6 @@ class ActionModel(QtGui.QStandardItemModel): def discover(self): """Set up Actions cache. Run this for each new project.""" - if not self.dbcon.Session.get("AVALON_PROJECT"): - self._registered_actions = list() - return - # Discover all registered actions actions = api.discover(api.Action) @@ -144,6 +140,9 @@ class ActionModel(QtGui.QStandardItemModel): def get_application_actions(self): actions = [] + if not self.dbcon.Session.get("AVALON_PROJECT"): + return actions + project_doc = self.dbcon.find_one({"type": "project"}) if not project_doc: return actions @@ -185,6 +184,8 @@ class ActionModel(QtGui.QStandardItemModel): self._groups.clear() + self.discover() + actions = self.filter_compatible_actions(self._registered_actions) self.beginResetModel() diff --git a/pype/tools/launcher/window.py b/pype/tools/launcher/window.py index 55635e2139..ac4558df8b 100644 --- a/pype/tools/launcher/window.py +++ b/pype/tools/launcher/window.py @@ -186,6 +186,7 @@ class AssetsPanel(QtWidgets.QWidget): # signals project_bar.project_changed.connect(self.on_project_changed) assets_widget.selection_changed.connect(self.on_asset_changed) + assets_widget.refreshed.connect(self.on_asset_changed) btn_back.clicked.connect(self.back_clicked) # Force initial refresh for the assets since we might not be @@ -206,9 +207,6 @@ class AssetsPanel(QtWidgets.QWidget): self.dbcon.Session["AVALON_PROJECT"] = project_name self.assets_widget.refresh() - # Force asset change callback to ensure tasks are correctly reset - self.assets_widget.refreshed.connect(self.on_asset_changed) - def on_asset_changed(self): """Callback on asset selection changed 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 88e3010603..72b7a8e130 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 @@ -12,23 +12,26 @@ "children": [{ "type": "text", "key": "AVALON_MONGO", - "label": "Avalon Mongo URL" + "label": "Avalon Mongo URL", + "placeholder": "Pype Mongo is used if not filled." + }, + { + "type": "number", + "key": "AVALON_TIMEOUT", + "minimum": 0, + "label": "Avalon Mongo Timeout (ms)" + }, + { + "type": "path-widget", + "label": "Thumbnail Storage Location", + "key": "AVALON_THUMBNAIL_ROOT", + "multiplatform": true, + "multipath": false }, { "type": "text", "key": "AVALON_DB_DATA", "label": "Avalon Mongo Data Location" - }, - { - "type": "text", - "key": "AVALON_THUMBNAIL_ROOT", - "label": "Thumbnail Storage Location" - }, - { - "key": "environment", - "label": "Environment", - "type": "raw-json", - "env_group_key": "avalon" } ] }, { diff --git a/pype/tools/tray/modules_imports.json b/pype/tools/tray/modules_imports.json deleted file mode 100644 index 499e5fc08c..0000000000 --- a/pype/tools/tray/modules_imports.json +++ /dev/null @@ -1,68 +0,0 @@ -[ - { - "title": "User settings", - "type": "module", - "import_path": "pype.modules.user", - "fromlist": ["pype", "modules"] - }, { - "title": "Ftrack", - "type": "module", - "import_path": "pype.modules.ftrack.tray", - "fromlist": ["pype", "modules", "ftrack"] - }, { - "title": "Muster", - "type": "module", - "import_path": "pype.modules.muster", - "fromlist": ["pype", "modules"] - }, { - "title": "Avalon", - "type": "module", - "import_path": "pype.modules.avalon_apps", - "fromlist": ["pype", "modules"] - }, { - "title": "Clockify", - "type": "module", - "import_path": "pype.modules.clockify", - "fromlist": ["pype", "modules"] - }, { - "title": "Standalone Publish", - "type": "module", - "import_path": "pype.modules.standalonepublish", - "fromlist": ["pype", "modules"] - }, { - "title": "Logging", - "type": "module", - "import_path": "pype.modules.logging.tray", - "fromlist": ["pype", "modules", "logging"] - }, { - "title": "Idle Manager", - "type": "module", - "import_path": "pype.modules.idle_manager", - "fromlist": ["pype","modules"] - }, { - "title": "Timers Manager", - "type": "module", - "import_path": "pype.modules.timers_manager", - "fromlist": ["pype","modules"] - }, { - "title": "Rest Api", - "type": "module", - "import_path": "pype.modules.rest_api", - "fromlist": ["pype","modules"] - }, { - "title": "Adobe Communicator", - "type": "module", - "import_path": "pype.modules.adobe_communicator", - "fromlist": ["pype", "modules"] - }, { - "title": "Websocket Server", - "type": "module", - "import_path": "pype.modules.websocket_server", - "fromlist": ["pype", "modules"] - }, { - "title": "Sync Server", - "type": "module", - "import_path": "pype.modules.sync_server", - "fromlist": ["pype","modules"] - } -] diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 75c62592e3..c8c04d229a 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -5,7 +5,8 @@ import platform from avalon import style from Qt import QtCore, QtGui, QtWidgets, QtSvg from pype.api import Logger, resources -from pype.settings.lib import get_system_settings, load_json_file +from pype.modules import TrayModulesManager, ITrayService +from pype.settings.lib import get_system_settings import pype.version try: import configparser @@ -26,89 +27,35 @@ class TrayManager: self.log = Logger().get_logger(self.__class__.__name__) - self.modules = {} - self.services = {} - self.services_submenu = None + self.module_settings = get_system_settings()["modules"] + + self.modules_manager = TrayModulesManager() self.errors = [] - CURRENT_DIR = os.path.dirname(__file__) - self.modules_imports = load_json_file( - os.path.join(CURRENT_DIR, "modules_imports.json") - ) - module_settings = get_system_settings()["modules"] - self.module_settings = module_settings + def initialize_modules(self): + """Add modules to tray.""" - self.icon_run = QtGui.QIcon( - resources.get_resource("icons", "circle_green.png") - ) - self.icon_stay = QtGui.QIcon( - resources.get_resource("icons", "circle_orange.png") - ) - self.icon_failed = QtGui.QIcon( - resources.get_resource("icons", "circle_red.png") - ) - - def process_presets(self): - """Add modules to tray by presets. - - This is start up method for TrayManager. Loads presets and import - modules described in "menu_items.json". In `item_usage` key you can - specify by item's title or import path if you want to import it. - Example of "menu_items.json" file: - { - "item_usage": { - "Statics Server": false - } - } - In this case `Statics Server` won't be used. - """ - - items = [] - # Get booleans is module should be used - for item in self.modules_imports: - import_path = item.get("import_path") - title = item.get("title") - - module_data = self.module_settings.get(title) - if not module_data: - if not title: - title = import_path - self.log.warning("{} - Module data not found".format(title)) - continue - - enabled = module_data.pop("enabled", True) - if not enabled: - self.log.debug("{} - Module is disabled".format(title)) - continue - - item["attributes"] = module_data - items.append(item) - - if items: - self.process_items(items, self.tray_widget.menu) + self.modules_manager.initialize(self.tray_widget.menu) # Add services if they are - if self.services_submenu is not None: - self.tray_widget.menu.addMenu(self.services_submenu) + services_submenu = ITrayService.services_submenu(self.tray_widget.menu) + self.tray_widget.menu.addMenu(services_submenu) # Add separator - if items and self.services_submenu is not None: - self.add_separator(self.tray_widget.menu) + self.tray_widget.menu.addSeparator() self._add_version_item() # Add Exit action to menu - aExit = QtWidgets.QAction("&Exit", self.tray_widget) - aExit.triggered.connect(self.tray_widget.exit) - self.tray_widget.menu.addAction(aExit) + exit_action = QtWidgets.QAction("Exit", self.tray_widget) + exit_action.triggered.connect(self.tray_widget.exit) + self.tray_widget.menu.addAction(exit_action) # Tell each module which modules were imported - self.connect_modules() - self.start_modules() + self.modules_manager.start_modules() def _add_version_item(self): - subversion = os.environ.get("PYPE_SUBVERSION") client_name = os.environ.get("PYPE_CLIENT") @@ -121,246 +68,10 @@ class TrayManager: version_action = QtWidgets.QAction(version_string, self.tray_widget) self.tray_widget.menu.addAction(version_action) - self.add_separator(self.tray_widget.menu) - - def process_items(self, items, parent_menu): - """ Loop through items and add them to parent_menu. - - :param items: contains dictionary objects representing each item - :type items: list - :param parent_menu: menu where items will be add - :type parent_menu: QtWidgets.QMenu - """ - for item in items: - i_type = item.get('type', None) - result = False - if i_type is None: - continue - elif i_type == 'module': - result = self.add_module(item, parent_menu) - elif i_type == 'action': - result = self.add_action(item, parent_menu) - elif i_type == 'menu': - result = self.add_menu(item, parent_menu) - elif i_type == 'separator': - result = self.add_separator(parent_menu) - - if result is False: - self.errors.append(item) - - def add_module(self, item, parent_menu): - """Inicialize object of module and add it to context. - - :param item: item from presets containing information about module - :type item: dict - :param parent_menu: menu where module's submenus/actions will be add - :type parent_menu: QtWidgets.QMenu - :returns: success of module implementation - :rtype: bool - - REQUIRED KEYS (item): - :import_path (*str*): - - full import path as python's import - - e.g. *"path.to.module"* - :fromlist (*list*): - - subparts of import_path (as from is used) - - e.g. *["path", "to"]* - OPTIONAL KEYS (item): - :title (*str*): - - represents label shown in services menu - - import_path is used if title is not set - - title is not used at all if module is not a service - - .. note:: - Module is added as **service** if object does not have - *tray_menu* method. - """ - import_path = item.get('import_path', None) - title = item.get('title', import_path) - fromlist = item.get('fromlist', []) - attributes = item.get("attributes", {}) - try: - module = __import__( - "{}".format(import_path), - fromlist=fromlist - ) - klass = getattr(module, "CLASS_DEFINIION", None) - if not klass and attributes: - self.log.debug(( - "There are defined attributes for module \"{}\" but" - "module does not have defined \"CLASS_DEFINIION\"." - ).format(import_path)) - - elif klass and attributes: - for key, value in attributes.items(): - if hasattr(klass, key): - setattr(klass, key, value) - else: - self.log.error(( - "Module \"{}\" does not have attribute \"{}\"." - " Check your settings please." - ).format(import_path, key)) - obj = module.tray_init(self.tray_widget, self.main_window) - name = obj.__class__.__name__ - if hasattr(obj, 'tray_menu'): - obj.tray_menu(parent_menu) - else: - if self.services_submenu is None: - self.services_submenu = QtWidgets.QMenu( - 'Services', self.tray_widget.menu - ) - action = QtWidgets.QAction(title, self.services_submenu) - action.setIcon(self.icon_run) - self.services_submenu.addAction(action) - if hasattr(obj, 'set_qaction'): - obj.set_qaction(action, self.icon_failed) - self.modules[name] = obj - self.log.info("{} - Module imported".format(title)) - except Exception as exc: - if self.services_submenu is None: - self.services_submenu = QtWidgets.QMenu( - 'Services', self.tray_widget.menu - ) - action = QtWidgets.QAction(title, self.services_submenu) - action.setIcon(self.icon_failed) - self.services_submenu.addAction(action) - self.log.warning( - "{} - Module import Error: {}".format(title, str(exc)), - exc_info=True - ) - return False - return True - - def add_action(self, item, parent_menu): - """Adds action to parent_menu. - - :param item: item from presets containing information about action - :type item: dictionary - :param parent_menu: menu where action will be added - :type parent_menu: QtWidgets.QMenu - :returns: success of adding item to parent_menu - :rtype: bool - - REQUIRED KEYS (item): - :title (*str*): - - represents label shown in menu - :sourcetype (*str*): - - type of action *enum["file", "python"]* - :command (*str*): - - filepath to script *(sourcetype=="file")* - - python code as string *(sourcetype=="python")* - OPTIONAL KEYS (item): - :tooltip (*str*): - - will be shown when hover over action - """ - sourcetype = item.get('sourcetype', None) - command = item.get('command', None) - title = item.get('title', '*ERROR*') - tooltip = item.get('tooltip', None) - - if sourcetype not in self.available_sourcetypes: - self.log.error('item "{}" has invalid sourcetype'.format(title)) - return False - if command is None or command.strip() == '': - self.log.error('item "{}" has invalid command'.format(title)) - return False - - new_action = QtWidgets.QAction(title, parent_menu) - if tooltip is not None and tooltip.strip() != '': - new_action.setToolTip(tooltip) - - if sourcetype == 'python': - new_action.triggered.connect( - lambda: exec(command) - ) - elif sourcetype == 'file': - command = os.path.normpath(command) - if '$' in command: - command_items = command.split(os.path.sep) - for i in range(len(command_items)): - if command_items[i].startswith('$'): - # TODO: raise error if environment was not found? - command_items[i] = os.environ.get( - command_items[i].replace('$', ''), command_items[i] - ) - command = os.path.sep.join(command_items) - - new_action.triggered.connect( - lambda: exec(open(command).read(), globals()) - ) - - parent_menu.addAction(new_action) - - def add_menu(self, item, parent_menu): - """ Adds submenu to parent_menu. - - :param item: item from presets containing information about menu - :type item: dictionary - :param parent_menu: menu where submenu will be added - :type parent_menu: QtWidgets.QMenu - :returns: success of adding item to parent_menu - :rtype: bool - - REQUIRED KEYS (item): - :title (*str*): - - represents label shown in menu - :items (*list*): - - list of submenus / actions / separators / modules *(dict)* - """ - try: - title = item.get('title', None) - if title is None or title.strip() == '': - self.log.error('Missing title in menu from presets') - return False - new_menu = QtWidgets.QMenu(title, parent_menu) - new_menu.setProperty('submenu', 'on') - parent_menu.addMenu(new_menu) - - self.process_items(item.get('items', []), new_menu) - return True - except Exception: - return False - - def add_separator(self, parent_menu): - """ Adds separator to parent_menu. - - :param parent_menu: menu where submenu will be added - :type parent_menu: QtWidgets.QMenu - :returns: success of adding item to parent_menu - :rtype: bool - """ - try: - parent_menu.addSeparator() - return True - except Exception: - return False - - def connect_modules(self): - """Sends all imported modules to imported modules - which have process_modules method. - """ - for obj in self.modules.values(): - if hasattr(obj, 'process_modules'): - obj.process_modules(self.modules) - - def start_modules(self): - """Modules which can be modified by another modules and - must be launched after *connect_modules* should have tray_start - to start their process afterwards. (e.g. Ftrack actions) - """ - for obj in self.modules.values(): - if hasattr(obj, 'tray_start'): - obj.tray_start() + self.tray_widget.menu.addSeparator() def on_exit(self): - for obj in self.modules.values(): - if hasattr(obj, 'tray_exit'): - try: - obj.tray_exit() - except Exception: - self.log.error("Failed to exit module {}".format( - obj.__class__.__name__ - )) + self.modules_manager.on_exit() class SystemTrayIcon(QtWidgets.QSystemTrayIcon): @@ -384,7 +95,7 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): # Set modules self.tray_man = TrayManager(self, self.parent) - self.tray_man.process_presets() + self.tray_man.initialize_modules() # Catch activate event self.activated.connect(self.on_systray_activated)