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)