diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py
index 7310e91e9b..f920e38765 100644
--- a/pype/hosts/harmony/__init__.py
+++ b/pype/hosts/harmony/__init__.py
@@ -1,5 +1,6 @@
import os
import sys
+from uuid import uuid4
from avalon import api, io, harmony
from avalon.vendor import Qt
@@ -8,8 +9,11 @@ import pyblish.api
from pype import lib
+signature = str(uuid4())
+
+
def set_scene_settings(settings):
- func = """function func(args)
+ func = """function %s_func(args)
{
if (args[0]["fps"])
{
@@ -36,8 +40,8 @@ def set_scene_settings(settings):
)
}
}
- func
- """
+ %s_func
+ """ % (signature, signature)
harmony.send({"function": func, "args": [settings]})
@@ -107,15 +111,15 @@ def check_inventory():
outdated_containers.append(container)
# Colour nodes.
- func = """function func(args){
+ func = """function %s_func(args){
for( var i =0; i <= args[0].length - 1; ++i)
{
var red_color = new ColorRGBA(255, 0, 0, 255);
node.setColor(args[0][i], red_color);
}
}
- func
- """
+ %s_func
+ """ % (signature, signature)
outdated_nodes = []
for container in outdated_containers:
if container["loader"] == "ImageSequenceLoader":
@@ -144,7 +148,7 @@ def application_launch():
def export_template(backdrops, nodes, filepath):
- func = """function func(args)
+ func = """function %s_func(args)
{
var temp_node = node.add("Top", "temp_note", "NOTE", 0, 0, 0);
@@ -179,8 +183,8 @@ def export_template(backdrops, nodes, filepath):
Action.perform("onActionUpToParent()", "Node View");
node.deleteNode(template_group, true, true);
}
- func
- """
+ %s_func
+ """ % (signature, signature)
harmony.send({
"function": func,
"args": [
@@ -221,12 +225,15 @@ def install():
def on_pyblish_instance_toggled(instance, old_value, new_value):
"""Toggle node enabling on instance toggles."""
- func = """function func(args)
+ func = """function %s_func(args)
{
node.setEnable(args[0], args[1])
}
- func
- """
- harmony.send(
- {"function": func, "args": [instance[0], new_value]}
- )
+ %s_func
+ """ % (signature, signature)
+ try:
+ harmony.send(
+ {"function": func, "args": [instance[0], new_value]}
+ )
+ except IndexError:
+ print(f"Instance '{instance}' is missing node")
diff --git a/pype/hosts/maya/menu.py b/pype/hosts/maya/menu.py
index 70ad8d31ca..9dadd8d1f5 100644
--- a/pype/hosts/maya/menu.py
+++ b/pype/hosts/maya/menu.py
@@ -32,8 +32,19 @@ def deferred():
command=lambda *args: BuildWorkfile().process()
)
+ def add_look_assigner_item():
+ import mayalookassigner
+ cmds.menuItem(
+ "Look assigner",
+ parent=pipeline._menu,
+ command=lambda *args: mayalookassigner.show()
+ )
+
log.info("Attempting to install scripts menu..")
+ add_build_workfiles_item()
+ add_look_assigner_item()
+
try:
import scriptsmenu.launchformaya as launchformaya
import scriptsmenu.scriptsmenu as scriptsmenu
@@ -42,7 +53,6 @@ def deferred():
"Skipping studio.menu install, because "
"'scriptsmenu' module seems unavailable."
)
- add_build_workfiles_item()
return
# load configuration of custom menu
diff --git a/pype/hosts/nukestudio/tags.py b/pype/hosts/nukestudio/tags.py
index c2b1d0d728..c8af0cabc1 100644
--- a/pype/hosts/nukestudio/tags.py
+++ b/pype/hosts/nukestudio/tags.py
@@ -71,8 +71,8 @@ def add_tags_from_presets():
# Get project task types.
tasks = io.find_one({"type": "project"})["config"]["tasks"]
nks_pres_tags["[Tasks]"] = {}
- for task in tasks:
- nks_pres_tags["[Tasks]"][task["name"]] = {
+ for task_type in tasks.keys():
+ nks_pres_tags["[Tasks]"][task_type] = {
"editable": "1",
"note": "",
"icon": {
diff --git a/pype/modules/avalon_apps/avalon_app.py b/pype/modules/avalon_apps/avalon_app.py
index 7ed651f82b..de10268304 100644
--- a/pype/modules/avalon_apps/avalon_app.py
+++ b/pype/modules/avalon_apps/avalon_app.py
@@ -1,16 +1,27 @@
-from Qt import QtWidgets
-from avalon.tools import libraryloader
from pype.api import Logger
-from pype.tools.launcher import LauncherWindow, actions
class AvalonApps:
def __init__(self, main_parent=None, parent=None):
self.log = Logger().get_logger(__name__)
- self.main_parent = main_parent
+
+ self.tray_init(main_parent, parent)
+
+ def tray_init(self, main_parent, parent):
+ from avalon.tools.libraryloader import app
+ from avalon import style
+ from pype.tools.launcher import LauncherWindow, actions
+
self.parent = parent
+ self.main_parent = main_parent
self.app_launcher = LauncherWindow()
+ self.libraryloader = app.Window(
+ icon=self.parent.icon,
+ show_projects=True,
+ show_libraries=True
+ )
+ self.libraryloader.setStyleSheet(style.load_stylesheet())
# actions.register_default_actions()
actions.register_config_actions()
@@ -23,6 +34,7 @@ class AvalonApps:
# Definition of Tray menu
def tray_menu(self, parent_menu=None):
+ from Qt import QtWidgets
# Actions
if parent_menu is None:
if self.parent is None:
@@ -52,9 +64,11 @@ class AvalonApps:
self.app_launcher.activateWindow()
def show_library_loader(self):
- libraryloader.show(
- parent=self.main_parent,
- icon=self.parent.icon,
- show_projects=True,
- show_libraries=True
- )
+ self.libraryloader.show()
+
+ # Raise and activate the window
+ # for MacOS
+ self.libraryloader.raise_()
+ # for Windows
+ self.libraryloader.activateWindow()
+ self.libraryloader.refresh()
diff --git a/pype/modules/clockify/clockify.py b/pype/modules/clockify/clockify.py
index fea15a1bea..4309bff9f2 100644
--- a/pype/modules/clockify/clockify.py
+++ b/pype/modules/clockify/clockify.py
@@ -1,9 +1,8 @@
import os
import threading
+import time
+
from pype.api import Logger
-from avalon import style
-from Qt import QtWidgets
-from .widgets import ClockifySettings, MessageWidget
from .clockify_api import ClockifyAPI
from .constants import CLOCKIFY_FTRACK_USER_PATH
@@ -17,11 +16,21 @@ class ClockifyModule:
os.environ["CLOCKIFY_WORKSPACE"] = self.workspace_name
+ self.timer_manager = None
+ self.MessageWidgetClass = None
+
+ self.clockapi = ClockifyAPI(master_parent=self)
+
self.log = Logger().get_logger(self.__class__.__name__, "PypeTray")
+ self.tray_init(main_parent, parent)
+
+ def tray_init(self, main_parent, parent):
+ from .widgets import ClockifySettings, MessageWidget
+
+ self.MessageWidgetClass = MessageWidget
self.main_parent = main_parent
self.parent = parent
- self.clockapi = ClockifyAPI(master_parent=self)
self.message_widget = None
self.widget_settings = ClockifySettings(main_parent, self)
self.widget_settings_required = None
@@ -57,11 +66,10 @@ class ClockifyModule:
)
if 'AvalonApps' in modules:
- from launcher import lib
- actions_path = os.path.sep.join([
+ actions_path = os.path.join(
os.path.dirname(__file__),
'launcher_actions'
- ])
+ )
current = os.environ.get('AVALON_ACTIONS', '')
if current:
current += os.pathsep
@@ -78,12 +86,12 @@ class ClockifyModule:
self.stop_timer()
def timer_started(self, data):
- if hasattr(self, 'timer_manager'):
+ if self.timer_manager:
self.timer_manager.start_timers(data)
def timer_stopped(self):
self.bool_timer_run = False
- if hasattr(self, 'timer_manager'):
+ if self.timer_manager:
self.timer_manager.stop_timers()
def start_timer_check(self):
@@ -102,7 +110,7 @@ class ClockifyModule:
self.thread_timer_check = None
def check_running(self):
- import time
+
while self.bool_thread_check_running is True:
bool_timer_run = False
if self.clockapi.get_in_progress() is not None:
@@ -156,15 +164,14 @@ class ClockifyModule:
self.timer_stopped()
def signed_in(self):
- if hasattr(self, 'timer_manager'):
- if not self.timer_manager:
- return
+ if not self.timer_manager:
+ return
- if not self.timer_manager.last_task:
- return
+ if not self.timer_manager.last_task:
+ return
- if self.timer_manager.is_running:
- self.start_timer_manager(self.timer_manager.last_task)
+ if self.timer_manager.is_running:
+ self.start_timer_manager(self.timer_manager.last_task)
def start_timer(self, input_data):
# If not api key is not entered then skip
@@ -197,11 +204,14 @@ class ClockifyModule:
"
Please inform your Project Manager."
).format(project_name, str(self.clockapi.workspace_name))
- self.message_widget = MessageWidget(
- self.main_parent, msg, "Clockify - Info Message"
- )
- self.message_widget.closed.connect(self.on_message_widget_close)
- self.message_widget.show()
+ 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()
return
@@ -227,31 +237,29 @@ class ClockifyModule:
# Definition of Tray menu
def tray_menu(self, parent_menu):
# Menu for Tray App
- self.menu = QtWidgets.QMenu('Clockify', parent_menu)
- self.menu.setProperty('submenu', 'on')
- self.menu.setStyleSheet(style.load_stylesheet())
+ from Qt import QtWidgets
+ menu = QtWidgets.QMenu("Clockify", parent_menu)
+ menu.setProperty("submenu", "on")
# Actions
- self.aShowSettings = QtWidgets.QAction(
- "Settings", self.menu
- )
- self.aStopTimer = QtWidgets.QAction(
- "Stop timer", self.menu
- )
+ action_show_settings = QtWidgets.QAction("Settings", menu)
+ action_stop_timer = QtWidgets.QAction("Stop timer", menu)
- self.menu.addAction(self.aShowSettings)
- self.menu.addAction(self.aStopTimer)
+ menu.addAction(action_show_settings)
+ menu.addAction(action_stop_timer)
- self.aShowSettings.triggered.connect(self.show_settings)
- self.aStopTimer.triggered.connect(self.stop_timer)
+ action_show_settings.triggered.connect(self.show_settings)
+ action_stop_timer.triggered.connect(self.stop_timer)
+
+ self.action_stop_timer = action_stop_timer
self.set_menu_visibility()
- parent_menu.addMenu(self.menu)
+ parent_menu.addMenu(menu)
def show_settings(self):
self.widget_settings.input_api_key.setText(self.clockapi.get_api_key())
self.widget_settings.show()
def set_menu_visibility(self):
- self.aStopTimer.setVisible(self.bool_timer_run)
+ self.action_stop_timer.setVisible(self.bool_timer_run)
diff --git a/pype/modules/clockify/launcher_actions/ClockifySync.py b/pype/modules/clockify/launcher_actions/ClockifySync.py
index a77c038076..422a346023 100644
--- a/pype/modules/clockify/launcher_actions/ClockifySync.py
+++ b/pype/modules/clockify/launcher_actions/ClockifySync.py
@@ -30,7 +30,7 @@ class ClockifySync(api.Action):
projects_info = {}
for project in projects_to_sync:
- task_types = [task['name'] for task in project['config']['tasks']]
+ task_types = project['config']['tasks'].keys()
projects_info[project['name']] = task_types
clockify_projects = self.clockapi.get_projects()
diff --git a/pype/modules/ftrack/__init__.py b/pype/modules/ftrack/__init__.py
index aa8f04bffb..fad771f084 100644
--- a/pype/modules/ftrack/__init__.py
+++ b/pype/modules/ftrack/__init__.py
@@ -1,2 +1,12 @@
-from .lib import *
+from . import ftrack_server
from .ftrack_server import FtrackServer, check_ftrack_url
+from .lib import BaseHandler, BaseEvent, BaseAction
+
+__all__ = (
+ "ftrack_server",
+ "FtrackServer",
+ "check_ftrack_url",
+ "BaseHandler",
+ "BaseEvent",
+ "BaseAction"
+)
diff --git a/pype/modules/ftrack/ftrack_server/__init__.py b/pype/modules/ftrack/ftrack_server/__init__.py
index fcae4e0690..9e3920b500 100644
--- a/pype/modules/ftrack/ftrack_server/__init__.py
+++ b/pype/modules/ftrack/ftrack_server/__init__.py
@@ -1,2 +1,8 @@
from .ftrack_server import FtrackServer
from .lib import check_ftrack_url
+
+
+__all__ = (
+ "FtrackServer",
+ "check_ftrack_url"
+)
diff --git a/pype/modules/ftrack/lib/custom_db_connector.py b/pype/modules/ftrack/ftrack_server/custom_db_connector.py
similarity index 71%
rename from pype/modules/ftrack/lib/custom_db_connector.py
rename to pype/modules/ftrack/ftrack_server/custom_db_connector.py
index d498d041dc..8a8ba4ccbb 100644
--- a/pype/modules/ftrack/lib/custom_db_connector.py
+++ b/pype/modules/ftrack/ftrack_server/custom_db_connector.py
@@ -16,9 +16,9 @@ import pymongo
from pype.api import decompose_url
-class NotActiveTable(Exception):
+class NotActiveCollection(Exception):
def __init__(self, *args, **kwargs):
- msg = "Active table is not set. (This is bug)"
+ msg = "Active collection is not set. (This is bug)"
if not (args or kwargs):
args = [msg]
super().__init__(*args, **kwargs)
@@ -40,12 +40,12 @@ def auto_reconnect(func):
return decorated
-def check_active_table(func):
+def check_active_collection(func):
"""Check if CustomDbConnector has active collection."""
@functools.wraps(func)
def decorated(obj, *args, **kwargs):
- if not obj.active_table:
- raise NotActiveTable()
+ if not obj.active_collection:
+ raise NotActiveCollection()
return func(obj, *args, **kwargs)
return decorated
@@ -55,7 +55,7 @@ class CustomDbConnector:
timeout = int(os.environ["AVALON_TIMEOUT"])
def __init__(
- self, uri, database_name, port=None, table_name=None
+ self, uri, database_name, port=None, collection_name=None
):
self._mongo_client = None
self._sentry_client = None
@@ -76,10 +76,10 @@ class CustomDbConnector:
self._port = port
self._database_name = database_name
- self.active_table = table_name
+ self.active_collection = collection_name
def __getitem__(self, key):
- # gives direct access to collection withou setting `active_table`
+ # gives direct access to collection withou setting `active_collection`
return self._database[key]
def __getattribute__(self, attr):
@@ -88,9 +88,11 @@ class CustomDbConnector:
try:
return super(CustomDbConnector, self).__getattribute__(attr)
except AttributeError:
- if self.active_table is None:
- raise NotActiveTable()
- return self._database[self.active_table].__getattribute__(attr)
+ if self.active_collection is None:
+ raise NotActiveCollection()
+ return self._database[self.active_collection].__getattribute__(
+ attr
+ )
def install(self):
"""Establish a persistent connection to the database"""
@@ -146,46 +148,30 @@ class CustomDbConnector:
self._is_installed = False
atexit.unregister(self.uninstall)
- def create_table(self, name, **options):
- if self.exist_table(name):
+ def collection_exists(self, collection_name):
+ return collection_name in self.collections()
+
+ def create_collection(self, name, **options):
+ if self.collection_exists(name):
return
return self._database.create_collection(name, **options)
- def exist_table(self, table_name):
- return table_name in self.tables()
-
- def create_table(self, name, **options):
- if self.exist_table(name):
- return
-
- return self._database.create_collection(name, **options)
-
- def exist_table(self, table_name):
- return table_name in self.tables()
-
- def tables(self):
- """List available tables
- Returns:
- list of table names
- """
- collection_names = self.collections()
- for table_name in collection_names:
- if table_name in ("system.indexes",):
- continue
- yield table_name
-
@auto_reconnect
def collections(self):
- return self._database.collection_names()
+ for col_name in self._database.collection_names():
+ if col_name not in ("system.indexes",):
+ yield col_name
- @check_active_table
+ @check_active_collection
@auto_reconnect
def insert_one(self, item, **options):
assert isinstance(item, dict), "item must be of type "
- return self._database[self.active_table].insert_one(item, **options)
+ return self._database[self.active_collection].insert_one(
+ item, **options
+ )
- @check_active_table
+ @check_active_collection
@auto_reconnect
def insert_many(self, items, ordered=True, **options):
# check if all items are valid
@@ -194,72 +180,74 @@ class CustomDbConnector:
assert isinstance(item, dict), "`item` must be of type "
options["ordered"] = ordered
- return self._database[self.active_table].insert_many(items, **options)
+ return self._database[self.active_collection].insert_many(
+ items, **options
+ )
- @check_active_table
+ @check_active_collection
@auto_reconnect
def find(self, filter, projection=None, sort=None, **options):
options["sort"] = sort
- return self._database[self.active_table].find(
+ return self._database[self.active_collection].find(
filter, projection, **options
)
- @check_active_table
+ @check_active_collection
@auto_reconnect
def find_one(self, filter, projection=None, sort=None, **options):
assert isinstance(filter, dict), "filter must be "
options["sort"] = sort
- return self._database[self.active_table].find_one(
+ return self._database[self.active_collection].find_one(
filter,
projection,
**options
)
- @check_active_table
+ @check_active_collection
@auto_reconnect
def replace_one(self, filter, replacement, **options):
- return self._database[self.active_table].replace_one(
+ return self._database[self.active_collection].replace_one(
filter, replacement, **options
)
- @check_active_table
+ @check_active_collection
@auto_reconnect
def update_one(self, filter, update, **options):
- return self._database[self.active_table].update_one(
+ return self._database[self.active_collection].update_one(
filter, update, **options
)
- @check_active_table
+ @check_active_collection
@auto_reconnect
def update_many(self, filter, update, **options):
- return self._database[self.active_table].update_many(
+ return self._database[self.active_collection].update_many(
filter, update, **options
)
- @check_active_table
+ @check_active_collection
@auto_reconnect
def distinct(self, **options):
- return self._database[self.active_table].distinct(**options)
+ return self._database[self.active_collection].distinct(**options)
- @check_active_table
+ @check_active_collection
@auto_reconnect
def drop_collection(self, name_or_collection, **options):
- return self._database[self.active_table].drop(
+ return self._database[self.active_collection].drop(
name_or_collection, **options
)
- @check_active_table
+ @check_active_collection
@auto_reconnect
def delete_one(self, filter, collation=None, **options):
options["collation"] = collation
- return self._database[self.active_table].delete_one(
+ return self._database[self.active_collection].delete_one(
filter, **options
)
- @check_active_table
+ @check_active_collection
@auto_reconnect
def delete_many(self, filter, collation=None, **options):
options["collation"] = collation
- return self._database[self.active_table].delete_many(
+ return self._database[self.active_collection].delete_many(
filter, **options
)
diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py
index ee6b1216dc..79b708b17a 100644
--- a/pype/modules/ftrack/ftrack_server/lib.py
+++ b/pype/modules/ftrack/ftrack_server/lib.py
@@ -26,7 +26,7 @@ from pype.api import (
compose_url
)
-from pype.modules.ftrack.lib.custom_db_connector import CustomDbConnector
+from .custom_db_connector import CustomDbConnector
TOPIC_STATUS_SERVER = "pype.event.server.status"
@@ -153,9 +153,9 @@ class StorerEventHub(SocketBaseEventHub):
class ProcessEventHub(SocketBaseEventHub):
hearbeat_msg = b"processor"
- uri, port, database, table_name = get_ftrack_event_mongo_info()
+ uri, port, database, collection_name = get_ftrack_event_mongo_info()
- is_table_created = False
+ is_collection_created = False
pypelog = Logger().get_logger("Session Processor")
def __init__(self, *args, **kwargs):
@@ -163,7 +163,7 @@ class ProcessEventHub(SocketBaseEventHub):
self.uri,
self.database,
self.port,
- self.table_name
+ self.collection_name
)
super(ProcessEventHub, self).__init__(*args, **kwargs)
@@ -184,7 +184,7 @@ class ProcessEventHub(SocketBaseEventHub):
"Error with Mongo access, probably permissions."
"Check if exist database with name \"{}\""
" and collection \"{}\" inside."
- ).format(self.database, self.table_name))
+ ).format(self.database, self.collection_name))
self.sock.sendall(b"MongoError")
sys.exit(0)
diff --git a/pype/modules/ftrack/ftrack_server/sub_event_storer.py b/pype/modules/ftrack/ftrack_server/sub_event_storer.py
index 1635f6cea3..2f4395c8db 100644
--- a/pype/modules/ftrack/ftrack_server/sub_event_storer.py
+++ b/pype/modules/ftrack/ftrack_server/sub_event_storer.py
@@ -12,7 +12,9 @@ from pype.modules.ftrack.ftrack_server.lib import (
get_ftrack_event_mongo_info,
TOPIC_STATUS_SERVER, TOPIC_STATUS_SERVER_RESULT
)
-from pype.modules.ftrack.lib.custom_db_connector import CustomDbConnector
+from pype.modules.ftrack.ftrack_server.custom_db_connector import (
+ CustomDbConnector
+)
from pype.api import Logger
log = Logger().get_logger("Event storer")
@@ -23,8 +25,8 @@ class SessionFactory:
session = None
-uri, port, database, table_name = get_ftrack_event_mongo_info()
-dbcon = CustomDbConnector(uri, database, port, table_name)
+uri, port, database, collection_name = get_ftrack_event_mongo_info()
+dbcon = CustomDbConnector(uri, database, port, collection_name)
# ignore_topics = ["ftrack.meta.connected"]
ignore_topics = []
@@ -200,7 +202,7 @@ def main(args):
"Error with Mongo access, probably permissions."
"Check if exist database with name \"{}\""
" and collection \"{}\" inside."
- ).format(database, table_name))
+ ).format(database, collection_name))
sock.sendall(b"MongoError")
finally:
diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py
index 03124ab10d..28114c7fdc 100644
--- a/pype/modules/ftrack/lib/avalon_sync.py
+++ b/pype/modules/ftrack/lib/avalon_sync.py
@@ -16,6 +16,7 @@ from bson.objectid import ObjectId
from bson.errors import InvalidId
from pymongo import UpdateOne
import ftrack_api
+from pype.api import config
log = Logger().get_logger(__name__)
@@ -23,9 +24,9 @@ log = Logger().get_logger(__name__)
# Current schemas for avalon types
EntitySchemas = {
- "project": "avalon-core:project-2.0",
+ "project": "avalon-core:project-2.1",
"asset": "avalon-core:asset-3.0",
- "config": "avalon-core:config-1.0"
+ "config": "avalon-core:config-1.1"
}
# Group name of custom attributes
@@ -50,7 +51,7 @@ def check_regex(name, entity_type, in_schema=None, schema_patterns=None):
if in_schema:
schema_name = in_schema
elif entity_type == "project":
- schema_name = "project-2.0"
+ schema_name = "project-2.1"
elif entity_type == "task":
schema_name = "task"
@@ -103,6 +104,14 @@ def get_pype_attr(session, split_hierarchical=True):
def from_dict_to_set(data):
+ """
+ Converts 'data' into $set part of MongoDB update command.
+ Args:
+ data: (dictionary) - up-to-date data from Ftrack
+
+ Returns:
+ (dictionary) - { "$set" : "{..}"}
+ """
result = {"$set": {}}
dict_queue = queue.Queue()
dict_queue.put((None, data))
@@ -114,7 +123,8 @@ def from_dict_to_set(data):
if _key is not None:
new_key = "{}.{}".format(_key, key)
- if not isinstance(value, dict):
+ if not isinstance(value, dict) or \
+ (isinstance(value, dict) and not bool(value)): # empty dic
result["$set"][new_key] = value
continue
dict_queue.put((new_key, value))
@@ -123,6 +133,8 @@ def from_dict_to_set(data):
def get_avalon_project_template(project_name):
"""Get avalon template
+ Args:
+ project_name: (string)
Returns:
dictionary with templates
"""
@@ -135,6 +147,16 @@ def get_avalon_project_template(project_name):
def get_project_apps(in_app_list):
+ """
+ Returns metadata information about apps in 'in_app_list' enhanced
+ from toml files.
+ Args:
+ in_app_list: (list) - names of applications
+
+ Returns:
+ tuple (list, dictionary) - list of dictionaries about apps
+ dictionary of warnings
+ """
apps = []
# TODO report
missing_toml_msg = "Missing config file for application"
@@ -239,6 +261,28 @@ def get_hierarchical_attributes(session, entity, attr_names, attr_defaults={}):
return hier_values
+def get_task_short_name(task_type):
+ """
+ Returns short name (code) for 'task_type'. Short name stored in
+ metadata dictionary in project.config per each 'task_type'.
+ Could be used in anatomy, paths etc.
+ If no appropriate short name is found in mapping, 'task_type' is
+ returned back unchanged.
+
+ Currently stores data in:
+ 'pype-config/presets/ftrack/project_defaults.json'
+ Args:
+ task_type: (string) - Animation | Modeling ...
+
+ Returns:
+ (string) - anim | model ...
+ """
+ presets = config.get_presets()['ftrack']['project_defaults']\
+ .get("task_short_names")
+
+ return presets.get(task_type, task_type)
+
+
class SyncEntitiesFactory:
dbcon = AvalonMongoDB()
@@ -378,7 +422,7 @@ class SyncEntitiesFactory:
"custom_attributes": {},
"hier_attrs": {},
"avalon_attrs": {},
- "tasks": []
+ "tasks": {}
})
for entity in all_project_entities:
@@ -389,7 +433,9 @@ class SyncEntitiesFactory:
continue
elif entity_type_low == "task":
- entities_dict[parent_id]["tasks"].append(entity["name"])
+ # enrich task info with additional metadata
+ task = {"type": entity["type"]["name"]}
+ entities_dict[parent_id]["tasks"][entity["name"]] = task
continue
entity_id = entity["id"]
@@ -416,6 +462,13 @@ class SyncEntitiesFactory:
@property
def avalon_ents_by_id(self):
+ """
+ Returns dictionary of avalon tracked entities (assets stored in
+ MongoDB) accessible by its '_id'
+ (mongo intenal ID - example ObjectId("5f48de5830a9467b34b69798"))
+ Returns:
+ (dictionary) - {"(_id)": whole entity asset}
+ """
if self._avalon_ents_by_id is None:
self._avalon_ents_by_id = {}
for entity in self.avalon_entities:
@@ -425,6 +478,14 @@ class SyncEntitiesFactory:
@property
def avalon_ents_by_ftrack_id(self):
+ """
+ Returns dictionary of Mongo ids of avalon tracked entities
+ (assets stored in MongoDB) accessible by its 'ftrackId'
+ (id from ftrack)
+ (example '431ee3f2-e91a-11ea-bfa4-92591a5b5e3e')
+ Returns:
+ (dictionary) - {"(ftrackId)": "_id"}
+ """
if self._avalon_ents_by_ftrack_id is None:
self._avalon_ents_by_ftrack_id = {}
for entity in self.avalon_entities:
@@ -437,6 +498,13 @@ class SyncEntitiesFactory:
@property
def avalon_ents_by_name(self):
+ """
+ Returns dictionary of Mongo ids of avalon tracked entities
+ (assets stored in MongoDB) accessible by its 'name'
+ (example 'Hero')
+ Returns:
+ (dictionary) - {"(name)": "_id"}
+ """
if self._avalon_ents_by_name is None:
self._avalon_ents_by_name = {}
for entity in self.avalon_entities:
@@ -446,6 +514,15 @@ class SyncEntitiesFactory:
@property
def avalon_ents_by_parent_id(self):
+ """
+ Returns dictionary of avalon tracked entities
+ (assets stored in MongoDB) accessible by its 'visualParent'
+ (example ObjectId("5f48de5830a9467b34b69798"))
+
+ Fills 'self._avalon_archived_ents' for performance
+ Returns:
+ (dictionary) - {"(_id)": whole entity}
+ """
if self._avalon_ents_by_parent_id is None:
self._avalon_ents_by_parent_id = collections.defaultdict(list)
for entity in self.avalon_entities:
@@ -458,6 +535,14 @@ class SyncEntitiesFactory:
@property
def avalon_archived_ents(self):
+ """
+ Returns list of archived assets from DB
+ (their "type" == 'archived_asset')
+
+ Fills 'self._avalon_archived_ents' for performance
+ Returns:
+ (list) of assets
+ """
if self._avalon_archived_ents is None:
self._avalon_archived_ents = [
ent for ent in self.dbcon.find({"type": "archived_asset"})
@@ -466,6 +551,14 @@ class SyncEntitiesFactory:
@property
def avalon_archived_by_name(self):
+ """
+ Returns list of archived assets from DB
+ (their "type" == 'archived_asset')
+
+ Fills 'self._avalon_archived_by_name' for performance
+ Returns:
+ (dictionary of lists) of assets accessible by asset name
+ """
if self._avalon_archived_by_name is None:
self._avalon_archived_by_name = collections.defaultdict(list)
for ent in self.avalon_archived_ents:
@@ -474,6 +567,14 @@ class SyncEntitiesFactory:
@property
def avalon_archived_by_id(self):
+ """
+ Returns dictionary of archived assets from DB
+ (their "type" == 'archived_asset')
+
+ Fills 'self._avalon_archived_by_id' for performance
+ Returns:
+ (dictionary) of assets accessible by asset mongo _id
+ """
if self._avalon_archived_by_id is None:
self._avalon_archived_by_id = {
str(ent["_id"]): ent for ent in self.avalon_archived_ents
@@ -482,6 +583,15 @@ class SyncEntitiesFactory:
@property
def avalon_archived_by_parent_id(self):
+ """
+ Returns dictionary of archived assets from DB per their's parent
+ (their "type" == 'archived_asset')
+
+ Fills 'self._avalon_archived_by_parent_id' for performance
+ Returns:
+ (dictionary of lists) of assets accessible by asset parent
+ mongo _id
+ """
if self._avalon_archived_by_parent_id is None:
self._avalon_archived_by_parent_id = collections.defaultdict(list)
for entity in self.avalon_archived_ents:
@@ -494,6 +604,14 @@ class SyncEntitiesFactory:
@property
def subsets_by_parent_id(self):
+ """
+ Returns dictionary of subsets from Mongo ("type": "subset")
+ grouped by their parent.
+
+ Fills 'self._subsets_by_parent_id' for performance
+ Returns:
+ (dictionary of lists)
+ """
if self._subsets_by_parent_id is None:
self._subsets_by_parent_id = collections.defaultdict(list)
for subset in self.dbcon.find({"type": "subset"}):
@@ -515,6 +633,11 @@ class SyncEntitiesFactory:
@property
def all_ftrack_names(self):
+ """
+ Returns lists of names of all entities in Ftrack
+ Returns:
+ (list)
+ """
return [
ent_dict["name"] for ent_dict in self.entities_dict.values() if (
ent_dict.get("name")
@@ -534,8 +657,9 @@ class SyncEntitiesFactory:
name = entity_dict["name"]
entity_type = entity_dict["entity_type"]
# Tasks must be checked too
- for task_name in entity_dict["tasks"]:
- passed = task_names.get(task_name)
+ for task in entity_dict["tasks"].items():
+ task_name, task = task
+ passed = task_name
if passed is None:
passed = check_regex(
task_name, "task", schema_patterns=_schema_patterns
@@ -1014,22 +1138,26 @@ class SyncEntitiesFactory:
if not msg or not items:
continue
self.report_items["warning"][msg] = items
-
+ tasks = {}
+ for tt in task_types:
+ tasks[tt["name"]] = {
+ "short_name": get_task_short_name(tt["name"])
+ }
self.entities_dict[id]["final_entity"]["config"] = {
- "tasks": [{"name": tt["name"]} for tt in task_types],
+ "tasks": tasks,
"apps": proj_apps
}
continue
ent_path_items = [ent["name"] for ent in entity["link"]]
- parents = ent_path_items[1:len(ent_path_items)-1:]
+ parents = ent_path_items[1:len(ent_path_items) - 1:]
hierarchy = ""
if len(parents) > 0:
hierarchy = os.path.sep.join(parents)
data["parents"] = parents
data["hierarchy"] = hierarchy
- data["tasks"] = self.entities_dict[id].pop("tasks", [])
+ data["tasks"] = self.entities_dict[id].pop("tasks", {})
self.entities_dict[id]["final_entity"]["data"] = data
self.entities_dict[id]["final_entity"]["type"] = "asset"
@@ -1141,7 +1269,7 @@ class SyncEntitiesFactory:
if not is_right and not else_match_better:
entity = entity_dict["entity"]
ent_path_items = [ent["name"] for ent in entity["link"]]
- parents = ent_path_items[1:len(ent_path_items)-1:]
+ parents = ent_path_items[1:len(ent_path_items) - 1:]
av_parents = av_ent_by_mongo_id["data"]["parents"]
if av_parents == parents:
is_right = True
@@ -1904,10 +2032,10 @@ class SyncEntitiesFactory:
filter = {"_id": ObjectId(mongo_id)}
change_data = from_dict_to_set(changes)
mongo_changes_bulk.append(UpdateOne(filter, change_data))
-
if not mongo_changes_bulk:
# TODO LOG
return
+ log.debug("mongo_changes_bulk:: {}".format(mongo_changes_bulk))
self.dbcon.bulk_write(mongo_changes_bulk)
def reload_parents(self, hierarchy_changing_ids):
diff --git a/pype/modules/ftrack/lib/ftrack_base_handler.py b/pype/modules/ftrack/lib/ftrack_base_handler.py
index ce6607d6bf..d322fbaf23 100644
--- a/pype/modules/ftrack/lib/ftrack_base_handler.py
+++ b/pype/modules/ftrack/lib/ftrack_base_handler.py
@@ -2,7 +2,7 @@ import functools
import time
from pype.api import Logger
import ftrack_api
-from pype.modules.ftrack.ftrack_server.lib import SocketSession
+from pype.modules.ftrack import ftrack_server
class MissingPermision(Exception):
@@ -41,7 +41,7 @@ class BaseHandler(object):
self.log = Logger().get_logger(self.__class__.__name__)
if not(
isinstance(session, ftrack_api.session.Session) or
- isinstance(session, SocketSession)
+ isinstance(session, ftrack_server.lib.SocketSession)
):
raise Exception((
"Session object entered with args is instance of \"{}\""
@@ -49,7 +49,7 @@ class BaseHandler(object):
).format(
str(type(session)),
str(ftrack_api.session.Session),
- str(SocketSession)
+ str(ftrack_server.lib.SocketSession)
))
self._session = session
diff --git a/pype/modules/ftrack/tray/login_dialog.py b/pype/modules/ftrack/tray/login_dialog.py
index 7730ee1609..94ad29e478 100644
--- a/pype/modules/ftrack/tray/login_dialog.py
+++ b/pype/modules/ftrack/tray/login_dialog.py
@@ -1,7 +1,7 @@
import os
import requests
from avalon import style
-from pype.modules.ftrack import credentials
+from pype.modules.ftrack.lib import credentials
from . import login_tools
from pype.api import resources
from Qt import QtCore, QtGui, QtWidgets
@@ -238,6 +238,8 @@ class CredentialsDialog(QtWidgets.QDialog):
# If there is an existing server thread running we need to stop it.
if self._login_server_thread:
+ if self._login_server_thread.isAlive():
+ self._login_server_thread.stop()
self._login_server_thread.join()
self._login_server_thread = None
diff --git a/pype/modules/ftrack/tray/login_tools.py b/pype/modules/ftrack/tray/login_tools.py
index e7d22fbc19..d3297eaa76 100644
--- a/pype/modules/ftrack/tray/login_tools.py
+++ b/pype/modules/ftrack/tray/login_tools.py
@@ -61,12 +61,17 @@ class LoginServerThread(threading.Thread):
def __init__(self, url, callback):
self.url = url
self.callback = callback
+ self._server = None
super(LoginServerThread, self).__init__()
def _handle_login(self, api_user, api_key):
'''Login to server with *api_user* and *api_key*.'''
self.callback(api_user, api_key)
+ def stop(self):
+ if self._server:
+ self._server.server_close()
+
def run(self):
'''Listen for events.'''
self._server = HTTPServer(
diff --git a/pype/modules/logging/gui/__init__.py b/pype/modules/logging/tray/gui/__init__.py
similarity index 100%
rename from pype/modules/logging/gui/__init__.py
rename to pype/modules/logging/tray/gui/__init__.py
diff --git a/pype/modules/logging/gui/app.py b/pype/modules/logging/tray/gui/app.py
similarity index 100%
rename from pype/modules/logging/gui/app.py
rename to pype/modules/logging/tray/gui/app.py
diff --git a/pype/modules/logging/gui/models.py b/pype/modules/logging/tray/gui/models.py
similarity index 100%
rename from pype/modules/logging/gui/models.py
rename to pype/modules/logging/tray/gui/models.py
diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/tray/gui/widgets.py
similarity index 100%
rename from pype/modules/logging/gui/widgets.py
rename to pype/modules/logging/tray/gui/widgets.py
diff --git a/pype/modules/logging/tray/logging_module.py b/pype/modules/logging/tray/logging_module.py
index 9b26d5d9bf..84b40f68e1 100644
--- a/pype/modules/logging/tray/logging_module.py
+++ b/pype/modules/logging/tray/logging_module.py
@@ -1,6 +1,4 @@
-from Qt import QtWidgets
from pype.api import Logger
-from ..gui.app import LogsWindow
class LoggingModule:
@@ -8,7 +6,13 @@ class LoggingModule:
self.parent = parent
self.log = Logger().get_logger(self.__class__.__name__, "logging")
+ self.window = None
+
+ self.tray_init(main_parent, parent)
+
+ def tray_init(self, main_parent, parent):
try:
+ from .gui.app import LogsWindow
self.window = LogsWindow()
self.tray_menu = self._tray_menu
except Exception:
@@ -18,12 +22,12 @@ class LoggingModule:
# Definition of Tray menu
def _tray_menu(self, parent_menu):
+ from Qt import QtWidgets
# Menu for Tray App
menu = QtWidgets.QMenu('Logging', parent_menu)
- # menu.setProperty('submenu', 'on')
show_action = QtWidgets.QAction("Show Logs", menu)
- show_action.triggered.connect(self.on_show_logs)
+ show_action.triggered.connect(self._show_logs_gui)
menu.addAction(show_action)
parent_menu.addMenu(menu)
@@ -34,5 +38,6 @@ class LoggingModule:
def process_modules(self, modules):
return
- def on_show_logs(self):
- self.window.show()
+ def _show_logs_gui(self):
+ if self.window:
+ self.window.show()
diff --git a/pype/modules/muster/muster.py b/pype/modules/muster/muster.py
index 629fb12635..beb30690ac 100644
--- a/pype/modules/muster/muster.py
+++ b/pype/modules/muster/muster.py
@@ -1,10 +1,7 @@
-import appdirs
-from avalon import style
-from Qt import QtWidgets
import os
import json
-from .widget_login import MusterLogin
-from avalon.vendor import requests
+import appdirs
+import requests
class MusterModule:
@@ -21,6 +18,11 @@ class MusterModule:
self.cred_path = os.path.join(
self.cred_folder_path, self.cred_filename
)
+ self.tray_init(main_parent, parent)
+
+ def tray_init(self, main_parent, parent):
+ from .widget_login import MusterLogin
+
self.main_parent = main_parent
self.parent = parent
self.widget_login = MusterLogin(main_parent, self)
@@ -38,10 +40,6 @@ class MusterModule:
pass
def process_modules(self, modules):
-
- def api_callback():
- self.aShowLogin.trigger()
-
if "RestApiServer" in modules:
def api_show_login():
self.aShowLogin.trigger()
@@ -51,13 +49,12 @@ class MusterModule:
# Definition of Tray menu
def tray_menu(self, parent):
- """
- Add **change credentials** option to tray menu.
- """
+ """Add **change credentials** option to tray menu."""
+ from Qt import QtWidgets
+
# Menu for Tray App
self.menu = QtWidgets.QMenu('Muster', parent)
self.menu.setProperty('submenu', 'on')
- self.menu.setStyleSheet(style.load_stylesheet())
# Actions
self.aShowLogin = QtWidgets.QAction(
@@ -91,9 +88,9 @@ class MusterModule:
if not MUSTER_REST_URL:
raise AttributeError("Muster REST API url not set")
params = {
- 'username': username,
- 'password': password
- }
+ 'username': username,
+ 'password': password
+ }
api_entry = '/api/login'
response = self._requests_post(
MUSTER_REST_URL + api_entry, params=params)
diff --git a/pype/modules/standalonepublish/standalonepublish_module.py b/pype/modules/standalonepublish/standalonepublish_module.py
index ed997bfd9f..f8bc0c6f24 100644
--- a/pype/modules/standalonepublish/standalonepublish_module.py
+++ b/pype/modules/standalonepublish/standalonepublish_module.py
@@ -2,7 +2,6 @@ import os
import sys
import subprocess
import pype
-from pype import tools
class StandAlonePublishModule:
@@ -30,6 +29,7 @@ class StandAlonePublishModule:
))
def show(self):
+ from pype import tools
standalone_publisher_tool_path = os.path.join(
os.path.dirname(tools.__file__),
"standalonepublish"
diff --git a/pype/modules/timers_manager/__init__.py b/pype/modules/timers_manager/__init__.py
index a8a478d7ae..9de205f088 100644
--- a/pype/modules/timers_manager/__init__.py
+++ b/pype/modules/timers_manager/__init__.py
@@ -1,5 +1,4 @@
from .timers_manager import TimersManager
-from .widget_user_idle import WidgetUserIdle
CLASS_DEFINIION = TimersManager
diff --git a/pype/modules/timers_manager/timers_manager.py b/pype/modules/timers_manager/timers_manager.py
index 82ba1013f0..62767c24f1 100644
--- a/pype/modules/timers_manager/timers_manager.py
+++ b/pype/modules/timers_manager/timers_manager.py
@@ -1,21 +1,7 @@
-from .widget_user_idle import WidgetUserIdle, SignalHandler
-from pype.api import Logger, config
+from pype.api import Logger
-class Singleton(type):
- """ Signleton implementation
- """
- _instances = {}
-
- def __call__(cls, *args, **kwargs):
- if cls not in cls._instances:
- cls._instances[cls] = super(
- Singleton, cls
- ).__call__(*args, **kwargs)
- return cls._instances[cls]
-
-
-class TimersManager(metaclass=Singleton):
+class TimersManager:
""" Handles about Timers.
Should be able to start/stop all timers at once.
@@ -41,7 +27,13 @@ class TimersManager(metaclass=Singleton):
self.idle_man = None
self.signal_handler = None
+
+ self.trat_init(tray_widget, main_widget)
+
+ def trat_init(self, tray_widget, main_widget):
+ from .widget_user_idle import WidgetUserIdle, SignalHandler
self.widget_user_idle = WidgetUserIdle(self, tray_widget)
+ self.signal_handler = SignalHandler(self)
def set_signal_times(self):
try:
@@ -119,7 +111,6 @@ class TimersManager(metaclass=Singleton):
"""
if 'IdleManager' in modules:
- self.signal_handler = SignalHandler(self)
if self.set_signal_times() is True:
self.register_to_idle_manager(modules['IdleManager'])
diff --git a/pype/modules/user/user_module.py b/pype/modules/user/user_module.py
index f2de9dc2fb..dc57fe4a63 100644
--- a/pype/modules/user/user_module.py
+++ b/pype/modules/user/user_module.py
@@ -3,8 +3,6 @@ import json
import getpass
import appdirs
-from Qt import QtWidgets
-from .widget_user import UserWidget
from pype.api import Logger
@@ -24,6 +22,12 @@ class UserModule:
self.cred_path = os.path.normpath(os.path.join(
self.cred_folder_path, self.cred_filename
))
+ self.widget_login = None
+
+ self.tray_init(main_parent, parent)
+
+ def tray_init(self, main_parent=None, parent=None):
+ from .widget_user import UserWidget
self.widget_login = UserWidget(self)
self.load_credentials()
@@ -66,6 +70,7 @@ class UserModule:
# Definition of Tray menu
def tray_menu(self, parent_menu):
+ from Qt import QtWidgets
"""Add menu or action to Tray(or parent)'s menu"""
action = QtWidgets.QAction("Username", parent_menu)
action.triggered.connect(self.show_widget)
@@ -121,7 +126,8 @@ class UserModule:
self.cred = {"username": username}
os.environ[self.env_name] = username
- self.widget_login.set_user(username)
+ if self.widget_login:
+ self.widget_login.set_user(username)
try:
file = open(self.cred_path, "w")
file.write(json.dumps(self.cred))
diff --git a/pype/modules/websocket_server/websocket_server.py b/pype/modules/websocket_server/websocket_server.py
index 1152c65e00..daf4b03103 100644
--- a/pype/modules/websocket_server/websocket_server.py
+++ b/pype/modules/websocket_server/websocket_server.py
@@ -31,12 +31,13 @@ class WebSocketServer():
self.client = None
self.handlers = {}
+ port = None
websocket_url = os.getenv("WEBSOCKET_URL")
if websocket_url:
parsed = urllib.parse.urlparse(websocket_url)
port = parsed.port
if not port:
- port = 8099 # fallback
+ port = 8098 # fallback
self.app = web.Application()
diff --git a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py
index 50a25457f5..7d4e0333d6 100644
--- a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py
+++ b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py
@@ -2,7 +2,6 @@ import sys
import six
import pyblish.api
from avalon import io
-from pprint import pformat
try:
from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_AUTO_SYNC
@@ -46,9 +45,6 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
hierarchy_context = self.context.data["hierarchyContext"]
- self.log.debug(
- f"__ hierarchy_context: `{pformat(hierarchy_context)}`")
-
self.session = self.context.data["ftrackSession"]
project_name = self.context.data["projectEntity"]["name"]
query = 'Project where full_name is "{}"'.format(project_name)
diff --git a/pype/plugins/global/publish/collect_anatomy_instance_data.py b/pype/plugins/global/publish/collect_anatomy_instance_data.py
index 44a4d43946..446f671b86 100644
--- a/pype/plugins/global/publish/collect_anatomy_instance_data.py
+++ b/pype/plugins/global/publish/collect_anatomy_instance_data.py
@@ -23,123 +23,256 @@ Provides:
import copy
import json
+import collections
from avalon import io
import pyblish.api
-class CollectAnatomyInstanceData(pyblish.api.InstancePlugin):
- """Collect Instance specific Anatomy data."""
+class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
+ """Collect Instance specific Anatomy data.
+
+ Plugin is running for all instances on context even not active instances.
+ """
order = pyblish.api.CollectorOrder + 0.49
label = "Collect Anatomy Instance data"
- def process(self, instance):
- # get all the stuff from the database
- anatomy_data = copy.deepcopy(instance.context.data["anatomyData"])
- project_entity = instance.context.data["projectEntity"]
- context_asset_entity = instance.context.data["assetEntity"]
- instance_asset_entity = instance.data.get("assetEntity")
+ def process(self, context):
+ self.log.info("Collecting anatomy data for all instances.")
- asset_name = instance.data["asset"]
+ self.fill_missing_asset_docs(context)
+ self.fill_latest_versions(context)
+ self.fill_anatomy_data(context)
- # There is possibility that assetEntity on instance is already set
- # which can happen in standalone publisher
- if (
- instance_asset_entity
- and instance_asset_entity["name"] == asset_name
- ):
- asset_entity = instance_asset_entity
+ self.log.info("Anatomy Data collection finished.")
- # Check if asset name is the same as what is in context
- # - they may be different, e.g. in NukeStudio
- elif context_asset_entity["name"] == asset_name:
- asset_entity = context_asset_entity
+ def fill_missing_asset_docs(self, context):
+ self.log.debug("Qeurying asset documents for instances.")
- else:
- asset_entity = io.find_one({
- "type": "asset",
- "name": asset_name,
- "parent": project_entity["_id"]
- })
+ context_asset_doc = context.data["assetEntity"]
- subset_name = instance.data["subset"]
- version_number = instance.data.get("version")
- latest_version = None
+ instances_with_missing_asset_doc = collections.defaultdict(list)
+ for instance in context:
+ instance_asset_doc = instance.data.get("assetEntity")
+ _asset_name = instance.data["asset"]
- if asset_entity:
- subset_entity = io.find_one({
- "type": "subset",
- "name": subset_name,
- "parent": asset_entity["_id"]
- })
+ # There is possibility that assetEntity on instance is already set
+ # which can happen in standalone publisher
+ if (
+ instance_asset_doc
+ and instance_asset_doc["name"] == _asset_name
+ ):
+ continue
+
+ # Check if asset name is the same as what is in context
+ # - they may be different, e.g. in NukeStudio
+ if context_asset_doc["name"] == _asset_name:
+ instance.data["assetEntity"] = context_asset_doc
- if subset_entity is None:
- self.log.debug("Subset entity does not exist yet.")
else:
- version_entity = io.find_one(
- {
- "type": "version",
- "parent": subset_entity["_id"]
- },
- sort=[("name", -1)]
- )
- if version_entity:
- latest_version = version_entity["name"]
+ instances_with_missing_asset_doc[_asset_name].append(instance)
- # If version is not specified for instance or context
- if version_number is None:
- # TODO we should be able to change default version by studio
- # preferences (like start with version number `0`)
- version_number = 1
- # use latest version (+1) if already any exist
- if latest_version is not None:
- version_number += int(latest_version)
+ if not instances_with_missing_asset_doc:
+ self.log.debug("All instances already had right asset document.")
+ return
- anatomy_updates = {
- "asset": asset_name,
- "family": instance.data["family"],
- "subset": subset_name,
- "version": version_number
+ asset_names = list(instances_with_missing_asset_doc.keys())
+ self.log.debug("Querying asset documents with names: {}".format(
+ ", ".join(["\"{}\"".format(name) for name in asset_names])
+ ))
+ asset_docs = io.find({
+ "type": "asset",
+ "name": {"$in": asset_names}
+ })
+ asset_docs_by_name = {
+ asset_doc["name"]: asset_doc
+ for asset_doc in asset_docs
}
- if (
- asset_entity
- and asset_entity["_id"] != context_asset_entity["_id"]
- ):
- parents = asset_entity["data"].get("parents") or list()
- anatomy_updates["hierarchy"] = "/".join(parents)
- task_name = instance.data.get("task")
- if task_name:
- anatomy_updates["task"] = task_name
+ not_found_asset_names = []
+ for asset_name, instances in instances_with_missing_asset_doc.items():
+ asset_doc = asset_docs_by_name.get(asset_name)
+ if not asset_doc:
+ not_found_asset_names.append(asset_name)
+ continue
- # Version should not be collected since may be instance
- anatomy_data.update(anatomy_updates)
+ for _instance in instances:
+ _instance.data["assetEntity"] = asset_doc
- resolution_width = instance.data.get("resolutionWidth")
- if resolution_width:
- anatomy_data["resolution_width"] = resolution_width
+ if not_found_asset_names:
+ joined_asset_names = ", ".join(
+ ["\"{}\"".format(name) for name in not_found_asset_names]
+ )
+ self.log.warning((
+ "Not found asset documents with names \"{}\"."
+ ).format(joined_asset_names))
- resolution_height = instance.data.get("resolutionHeight")
- if resolution_height:
- anatomy_data["resolution_height"] = resolution_height
+ def fill_latest_versions(self, context):
+ """Try to find latest version for each instance's subset.
- pixel_aspect = instance.data.get("pixelAspect")
- if pixel_aspect:
- anatomy_data["pixel_aspect"] = float("{:0.2f}".format(
- float(pixel_aspect)))
+ Key "latestVersion" is always set to latest version or `None`.
- fps = instance.data.get("fps")
- if fps:
- anatomy_data["fps"] = float("{:0.2f}".format(
- float(fps)))
+ Args:
+ context (pyblish.Context)
- instance.data["projectEntity"] = project_entity
- instance.data["assetEntity"] = asset_entity
- instance.data["anatomyData"] = anatomy_data
- instance.data["latestVersion"] = latest_version
- # TODO should be version number set here?
- instance.data["version"] = version_number
+ Returns:
+ None
- self.log.info("Instance anatomy Data collected")
- self.log.debug(json.dumps(anatomy_data, indent=4))
+ """
+ self.log.debug("Qeurying latest versions for instances.")
+
+ hierarchy = {}
+ subset_names = set()
+ asset_ids = set()
+ for instance in context:
+ # Make sure `"latestVersion"` key is set
+ latest_version = instance.data.get("latestVersion")
+ instance.data["latestVersion"] = latest_version
+
+ # Skip instances withou "assetEntity"
+ asset_doc = instance.data.get("assetEntity")
+ if not asset_doc:
+ continue
+
+ # Store asset ids and subset names for queries
+ asset_id = asset_doc["_id"]
+ subset_name = instance.data["subset"]
+ asset_ids.add(asset_id)
+ subset_names.add(subset_name)
+
+ # Prepare instance hiearchy for faster filling latest versions
+ if asset_id not in hierarchy:
+ hierarchy[asset_id] = {}
+ if subset_name not in hierarchy[asset_id]:
+ hierarchy[asset_id][subset_name] = []
+ hierarchy[asset_id][subset_name].append(instance)
+
+ subset_docs = list(io.find({
+ "type": "subset",
+ "parent": {"$in": list(asset_ids)},
+ "name": {"$in": list(subset_names)}
+ }))
+
+ subset_ids = [
+ subset_doc["_id"]
+ for subset_doc in subset_docs
+ ]
+
+ last_version_by_subset_id = self._query_last_versions(subset_ids)
+ for subset_doc in subset_docs:
+ subset_id = subset_doc["_id"]
+ last_version = last_version_by_subset_id.get(subset_id)
+ if last_version is None:
+ continue
+
+ asset_id = subset_doc["parent"]
+ subset_name = subset_doc["name"]
+ _instances = hierarchy[asset_id][subset_name]
+ for _instance in _instances:
+ _instance.data["latestVersion"] = last_version
+
+ def _query_last_versions(self, subset_ids):
+ """Retrieve all latest versions for entered subset_ids.
+
+ Args:
+ subset_ids (list): List of subset ids with type `ObjectId`.
+
+ Returns:
+ dict: Key is subset id and value is last version name.
+ """
+ _pipeline = [
+ # Find all versions of those subsets
+ {"$match": {
+ "type": "version",
+ "parent": {"$in": subset_ids}
+ }},
+ # Sorting versions all together
+ {"$sort": {"name": 1}},
+ # Group them by "parent", but only take the last
+ {"$group": {
+ "_id": "$parent",
+ "_version_id": {"$last": "$_id"},
+ "name": {"$last": "$name"}
+ }}
+ ]
+
+ last_version_by_subset_id = {}
+ for doc in io.aggregate(_pipeline):
+ subset_id = doc["_id"]
+ last_version_by_subset_id[subset_id] = doc["name"]
+
+ return last_version_by_subset_id
+
+ def fill_anatomy_data(self, context):
+ self.log.debug("Storing anatomy data to instance data.")
+
+ project_doc = context.data["projectEntity"]
+ context_asset_doc = context.data["assetEntity"]
+
+ for instance in context:
+ version_number = instance.data.get("version")
+ # If version is not specified for instance or context
+ if version_number is None:
+ # TODO we should be able to change default version by studio
+ # preferences (like start with version number `0`)
+ version_number = 1
+ # use latest version (+1) if already any exist
+ latest_version = instance.data["latestVersion"]
+ if latest_version is not None:
+ version_number += int(latest_version)
+
+ anatomy_updates = {
+ "asset": instance.data["asset"],
+ "family": instance.data["family"],
+ "subset": instance.data["subset"],
+ "version": version_number
+ }
+
+ # Hiearchy
+ asset_doc = instance.data.get("assetEntity")
+ if asset_doc and asset_doc["_id"] != context_asset_doc["_id"]:
+ parents = asset_doc["data"].get("parents") or list()
+ anatomy_updates["hierarchy"] = "/".join(parents)
+
+ # Task
+ task_name = instance.data.get("task")
+ if task_name:
+ anatomy_updates["task"] = task_name
+
+ # Additional data
+ resolution_width = instance.data.get("resolutionWidth")
+ if resolution_width:
+ anatomy_updates["resolution_width"] = resolution_width
+
+ resolution_height = instance.data.get("resolutionHeight")
+ if resolution_height:
+ anatomy_updates["resolution_height"] = resolution_height
+
+ pixel_aspect = instance.data.get("pixelAspect")
+ if pixel_aspect:
+ anatomy_updates["pixel_aspect"] = float(
+ "{:0.2f}".format(float(pixel_aspect))
+ )
+
+ fps = instance.data.get("fps")
+ if fps:
+ anatomy_updates["fps"] = float("{:0.2f}".format(float(fps)))
+
+ anatomy_data = copy.deepcopy(context.data["anatomyData"])
+ anatomy_data.update(anatomy_updates)
+
+ # Store anatomy data
+ instance.data["projectEntity"] = project_doc
+ instance.data["anatomyData"] = anatomy_data
+ instance.data["version"] = version_number
+
+ # Log collected data
+ instance_name = instance.data["name"]
+ instance_label = instance.data.get("label")
+ if instance_label:
+ instance_name += "({})".format(instance_label)
+ self.log.debug("Anatomy data for instance {}: {}".format(
+ instance_name,
+ json.dumps(anatomy_data, indent=4)
+ ))
diff --git a/pype/plugins/global/publish/extract_hierarchy_avalon.py b/pype/plugins/global/publish/extract_hierarchy_avalon.py
index 4253c35929..2d82eca6b2 100644
--- a/pype/plugins/global/publish/extract_hierarchy_avalon.py
+++ b/pype/plugins/global/publish/extract_hierarchy_avalon.py
@@ -1,6 +1,6 @@
import pyblish.api
from avalon import io
-
+from copy import deepcopy
class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin):
"""Create entities in Avalon based on collected data."""
@@ -14,14 +14,12 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin):
if "hierarchyContext" not in context.data:
self.log.info("skipping IntegrateHierarchyToAvalon")
return
+ hierarchy_context = deepcopy(context.data["hierarchyContext"])
if not io.Session:
io.install()
active_assets = []
- hierarchy_context = context.data["hierarchyContext"]
- hierarchy_assets = self._get_assets(hierarchy_context)
-
# filter only the active publishing insatnces
for instance in context:
if instance.data.get("publish") is False:
@@ -32,13 +30,13 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin):
active_assets.append(instance.data["asset"])
- # filter out only assets which are activated as isntances
- new_hierarchy_assets = {k: v for k, v in hierarchy_assets.items()
- if k in active_assets}
+ # remove duplicity in list
+ self.active_assets = list(set(active_assets))
+ self.log.debug("__ self.active_assets: {}".format(self.active_assets))
- # modify the hierarchy context so there are only fitred assets
- self._set_assets(hierarchy_context, new_hierarchy_assets)
+ hierarchy_context = self._get_assets(hierarchy_context)
+ self.log.debug("__ hierarchy_context: {}".format(hierarchy_context))
input_data = context.data["hierarchyContext"] = hierarchy_context
self.project = None
@@ -61,7 +59,7 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin):
data["inputs"] = entity_data.get("inputs", [])
# Tasks.
- tasks = entity_data.get("tasks", [])
+ tasks = entity_data.get("tasks", {})
if tasks is not None or len(tasks) > 0:
data["tasks"] = tasks
parents = []
@@ -101,11 +99,13 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin):
if entity:
# Do not override data, only update
cur_entity_data = entity.get("data") or {}
- new_tasks = data.pop("tasks", [])
+ new_tasks = data.pop("tasks", {})
if "tasks" in cur_entity_data and new_tasks:
- for task_name in new_tasks:
- if task_name not in cur_entity_data["tasks"]:
- cur_entity_data["tasks"].append(task_name)
+ for task_name in new_tasks.keys():
+ if task_name \
+ not in cur_entity_data["tasks"].keys():
+ cur_entity_data["tasks"][task_name] = \
+ new_tasks[task_name]
cur_entity_data.update(data)
data = cur_entity_data
else:
@@ -178,35 +178,18 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin):
Usually the last part of deep dictionary which
is not having any children
"""
+ input_dict_copy = deepcopy(input_dict)
+
for key in input_dict.keys():
+ self.log.debug("__ key: {}".format(key))
# check if child key is available
if input_dict[key].get("childs"):
# loop deeper
- return self._get_assets(input_dict[key]["childs"])
+ input_dict_copy[key]["childs"] = self._get_assets(
+ input_dict[key]["childs"])
else:
- # give the dictionary with assets
- return input_dict
+ # filter out unwanted assets
+ if key not in self.active_assets:
+ input_dict_copy.pop(key, None)
- def _set_assets(self, input_dict, new_assets=None):
- """ Modify the hierarchy context dictionary.
- It will replace the asset dictionary with only the filtred one.
- """
- for key in input_dict.keys():
- # check if child key is available
- if input_dict[key].get("childs"):
- # return if this is just for testing purpose and no
- # new_assets property is avalable
- if not new_assets:
- return True
-
- # test for deeper inner children availabelity
- if self._set_assets(input_dict[key]["childs"]):
- # if one level deeper is still children available
- # then process farther
- self._set_assets(input_dict[key]["childs"], new_assets)
- else:
- # or just assign the filtred asset ditionary
- input_dict[key]["childs"] = new_assets
- else:
- # test didnt find more childs in input dictionary
- return None
+ return input_dict_copy
diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py
index 0bae1b2ddc..318c843b80 100644
--- a/pype/plugins/global/publish/extract_review.py
+++ b/pype/plugins/global/publish/extract_review.py
@@ -633,6 +633,26 @@ class ExtractReview(pyblish.api.InstancePlugin):
input_width = int(input_data["width"])
input_height = int(input_data["height"])
+ # Make sure input width and height is not an odd number
+ input_width_is_odd = bool(input_width % 2 != 0)
+ input_height_is_odd = bool(input_height % 2 != 0)
+ if input_width_is_odd or input_height_is_odd:
+ # Add padding to input and make sure this filter is at first place
+ filters.append("pad=width=ceil(iw/2)*2:height=ceil(ih/2)*2")
+
+ # Change input width or height as first filter will change them
+ if input_width_is_odd:
+ self.log.info((
+ "Converting input width from odd to even number. {} -> {}"
+ ).format(input_width, input_width + 1))
+ input_width += 1
+
+ if input_height_is_odd:
+ self.log.info((
+ "Converting input height from odd to even number. {} -> {}"
+ ).format(input_height, input_height + 1))
+ input_height += 1
+
self.log.debug("pixel_aspect: `{}`".format(pixel_aspect))
self.log.debug("input_width: `{}`".format(input_width))
self.log.debug("input_height: `{}`".format(input_height))
@@ -654,6 +674,22 @@ class ExtractReview(pyblish.api.InstancePlugin):
output_width = int(output_width)
output_height = int(output_height)
+ # Make sure output width and height is not an odd number
+ # When this can happen:
+ # - if output definition has set width and height with odd number
+ # - `instance.data` contain width and height with odd numbeer
+ if output_width % 2 != 0:
+ self.log.warning((
+ "Converting output width from odd to even number. {} -> {}"
+ ).format(output_width, output_width + 1))
+ output_width += 1
+
+ if output_height % 2 != 0:
+ self.log.warning((
+ "Converting output height from odd to even number. {} -> {}"
+ ).format(output_height, output_height + 1))
+ output_height += 1
+
self.log.debug(
"Output resolution is {}x{}".format(output_width, output_height)
)
diff --git a/pype/plugins/maya/load/load_look.py b/pype/plugins/maya/load/load_look.py
index b9c0d81104..c5b58c9bd5 100644
--- a/pype/plugins/maya/load/load_look.py
+++ b/pype/plugins/maya/load/load_look.py
@@ -3,6 +3,8 @@ from avalon import api, io
import json
import pype.hosts.maya.lib
from collections import defaultdict
+from pype.widgets.message_window import ScrollMessageBox
+from Qt import QtWidgets
class LookLoader(pype.hosts.maya.plugin.ReferenceLoader):
@@ -44,18 +46,33 @@ class LookLoader(pype.hosts.maya.plugin.ReferenceLoader):
self.update(container, representation)
def update(self, container, representation):
+ """
+ Called by Scene Inventory when look should be updated to current
+ version.
+ If any reference edits cannot be applied, eg. shader renamed and
+ material not present, reference is unloaded and cleaned.
+ All failed edits are highlighted to the user via message box.
+ Args:
+ container: object that has look to be updated
+ representation: (dict): relationship data to get proper
+ representation from DB and persisted
+ data in .json
+ Returns:
+ None
+ """
import os
from maya import cmds
-
node = container["objectName"]
-
path = api.get_representation_path(representation)
# Get reference node from container members
members = cmds.sets(node, query=True, nodesOnly=True)
reference_node = self._get_reference_node(members)
+ shader_nodes = cmds.ls(members, type='shadingEngine')
+ orig_nodes = set(self._get_nodes_with_shader(shader_nodes))
+
file_type = {
"ma": "mayaAscii",
"mb": "mayaBinary",
@@ -66,6 +83,104 @@ class LookLoader(pype.hosts.maya.plugin.ReferenceLoader):
assert os.path.exists(path), "%s does not exist." % path
+ self._load_reference(file_type, node, path, reference_node)
+
+ # Remove any placeHolderList attribute entries from the set that
+ # are remaining from nodes being removed from the referenced file.
+ members = cmds.sets(node, query=True)
+ invalid = [x for x in members if ".placeHolderList" in x]
+ if invalid:
+ cmds.sets(invalid, remove=node)
+
+ # get new applied shaders and nodes from new version
+ shader_nodes = cmds.ls(members, type='shadingEngine')
+ nodes = set(self._get_nodes_with_shader(shader_nodes))
+
+ json_representation = io.find_one({
+ "type": "representation",
+ "parent": representation['parent'],
+ "name": "json"
+ })
+
+ # Load relationships
+ shader_relation = api.get_representation_path(json_representation)
+ with open(shader_relation, "r") as f:
+ relationships = json.load(f)
+
+ # update of reference could result in failed edits - material is not
+ # present because of renaming etc.
+ failed_edits = cmds.referenceQuery(reference_node,
+ editStrings=True,
+ failedEdits=True,
+ successfulEdits=False)
+
+ # highlight failed edits to user
+ if failed_edits:
+ # clean references - removes failed reference edits
+ cmds.file(cr=reference_node) # cleanReference
+
+ # reapply shading groups from json representation on orig nodes
+ pype.hosts.maya.lib.apply_shaders(relationships,
+ shader_nodes,
+ orig_nodes)
+
+ msg = ["During reference update some edits failed.",
+ "All successful edits were kept intact.\n",
+ "Failed and removed edits:"]
+ msg.extend(failed_edits)
+ msg = ScrollMessageBox(QtWidgets.QMessageBox.Warning,
+ "Some reference edit failed",
+ msg)
+ msg.exec_()
+
+ attributes = relationships.get("attributes", [])
+
+ # region compute lookup
+ nodes_by_id = defaultdict(list)
+ for n in nodes:
+ nodes_by_id[pype.hosts.maya.lib.get_id(n)].append(n)
+ pype.hosts.maya.lib.apply_attributes(attributes, nodes_by_id)
+
+ # Update metadata
+ cmds.setAttr("{}.representation".format(node),
+ str(representation["_id"]),
+ type="string")
+
+ def _get_nodes_with_shader(self, shader_nodes):
+ """
+ Returns list of nodes belonging to specific shaders
+ Args:
+ shader_nodes: of Shader groups
+ Returns
+ node names
+ """
+ import maya.cmds as cmds
+ # Get container members
+
+ nodes_list = []
+ for shader in shader_nodes:
+ connections = cmds.listConnections(cmds.listHistory(shader, f=1),
+ type='mesh')
+ if connections:
+ for connection in connections:
+ nodes_list.extend(cmds.listRelatives(connection,
+ shapes=True))
+ return nodes_list
+
+ def _load_reference(self, file_type, node, path, reference_node):
+ """
+ Load reference from 'path' on 'reference_node'. Used when change
+ of look (version/update) is triggered.
+ Args:
+ file_type: extension of referenced file
+ node:
+ path: (string) location of referenced file
+ reference_node: (string) - name of node that should be applied
+ on
+ Returns:
+ None
+ """
+ import maya.cmds as cmds
try:
content = cmds.file(path,
loadReference=reference_node,
@@ -86,57 +201,10 @@ class LookLoader(pype.hosts.maya.plugin.ReferenceLoader):
raise
self.log.warning("Ignoring file read error:\n%s", exc)
-
# Fix PLN-40 for older containers created with Avalon that had the
# `.verticesOnlySet` set to True.
if cmds.getAttr("{}.verticesOnlySet".format(node)):
self.log.info("Setting %s.verticesOnlySet to False", node)
cmds.setAttr("{}.verticesOnlySet".format(node), False)
-
# Add new nodes of the reference to the container
cmds.sets(content, forceElement=node)
-
- # Remove any placeHolderList attribute entries from the set that
- # are remaining from nodes being removed from the referenced file.
- members = cmds.sets(node, query=True)
- invalid = [x for x in members if ".placeHolderList" in x]
- if invalid:
- cmds.sets(invalid, remove=node)
-
- # Get container members
- shader_nodes = cmds.ls(members, type='shadingEngine')
-
- nodes_list = []
- for shader in shader_nodes:
- connections = cmds.listConnections(cmds.listHistory(shader, f=1),
- type='mesh')
- if connections:
- for connection in connections:
- nodes_list.extend(cmds.listRelatives(connection,
- shapes=True))
- nodes = set(nodes_list)
-
- json_representation = io.find_one({
- "type": "representation",
- "parent": representation['parent'],
- "name": "json"
- })
-
- # Load relationships
- shader_relation = api.get_representation_path(json_representation)
- with open(shader_relation, "r") as f:
- relationships = json.load(f)
-
- attributes = relationships.get("attributes", [])
-
- # region compute lookup
- nodes_by_id = defaultdict(list)
- for n in nodes:
- nodes_by_id[pype.hosts.maya.lib.get_id(n)].append(n)
-
- pype.hosts.maya.lib.apply_attributes(attributes, nodes_by_id)
-
- # Update metadata
- cmds.setAttr("{}.representation".format(node),
- str(representation["_id"]),
- type="string")
diff --git a/pype/plugins/maya/publish/collect_yeti_cache.py b/pype/plugins/maya/publish/collect_yeti_cache.py
index 4af3e1ea18..26c3f601f6 100644
--- a/pype/plugins/maya/publish/collect_yeti_cache.py
+++ b/pype/plugins/maya/publish/collect_yeti_cache.py
@@ -30,7 +30,6 @@ class CollectYetiCache(pyblish.api.InstancePlugin):
label = "Collect Yeti Cache"
families = ["yetiRig", "yeticache"]
hosts = ["maya"]
- tasks = ["animation", "fx"]
def process(self, instance):
diff --git a/pype/plugins/maya/publish/extract_camera_mayaScene.py b/pype/plugins/maya/publish/extract_camera_mayaScene.py
index 03dde031e9..1a0f4694d1 100644
--- a/pype/plugins/maya/publish/extract_camera_mayaScene.py
+++ b/pype/plugins/maya/publish/extract_camera_mayaScene.py
@@ -101,7 +101,7 @@ class ExtractCameraMayaScene(pype.api.Extractor):
self.log.info(
"Using {} as scene type".format(self.scene_type))
break
- except AttributeError:
+ except KeyError:
# no preset found
pass
diff --git a/pype/plugins/maya/publish/extract_maya_scene_raw.py b/pype/plugins/maya/publish/extract_maya_scene_raw.py
index 2971572552..d273646af8 100644
--- a/pype/plugins/maya/publish/extract_maya_scene_raw.py
+++ b/pype/plugins/maya/publish/extract_maya_scene_raw.py
@@ -33,7 +33,7 @@ class ExtractMayaSceneRaw(pype.api.Extractor):
self.log.info(
"Using {} as scene type".format(self.scene_type))
break
- except AttributeError:
+ except KeyError:
# no preset found
pass
# Define extract output file path
diff --git a/pype/plugins/maya/publish/extract_model.py b/pype/plugins/maya/publish/extract_model.py
index 330e471e53..d77e65f989 100644
--- a/pype/plugins/maya/publish/extract_model.py
+++ b/pype/plugins/maya/publish/extract_model.py
@@ -41,7 +41,7 @@ class ExtractModel(pype.api.Extractor):
self.log.info(
"Using {} as scene type".format(self.scene_type))
break
- except AttributeError:
+ except KeyError:
# no preset found
pass
# Define extract output file path
diff --git a/pype/plugins/maya/publish/extract_yeti_rig.py b/pype/plugins/maya/publish/extract_yeti_rig.py
index 2f66d3e026..d48a956b88 100644
--- a/pype/plugins/maya/publish/extract_yeti_rig.py
+++ b/pype/plugins/maya/publish/extract_yeti_rig.py
@@ -111,7 +111,7 @@ class ExtractYetiRig(pype.api.Extractor):
self.log.info(
"Using {} as scene type".format(self.scene_type))
break
- except AttributeError:
+ except KeyError:
# no preset found
pass
yeti_nodes = cmds.ls(instance, type="pgYetiMaya")
diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py
index e4048592a7..7509b5875a 100644
--- a/pype/plugins/maya/publish/submit_maya_deadline.py
+++ b/pype/plugins/maya/publish/submit_maya_deadline.py
@@ -262,6 +262,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
use_published = True
tile_assembler_plugin = "PypeTileAssembler"
+ asset_dependencies = False
def process(self, instance):
"""Plugin entry point."""
@@ -417,9 +418,10 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
# Adding file dependencies.
dependencies = instance.context.data["fileDependencies"]
dependencies.append(filepath)
- for dependency in dependencies:
- key = "AssetDependency" + str(dependencies.index(dependency))
- payload_skeleton["JobInfo"][key] = dependency
+ if self.assembly_files:
+ for dependency in dependencies:
+ key = "AssetDependency" + str(dependencies.index(dependency))
+ payload_skeleton["JobInfo"][key] = dependency
# Handle environments -----------------------------------------------
# We need those to pass them to pype for it to set correct context
@@ -731,10 +733,14 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
def _get_maya_payload(self, data):
payload = copy.deepcopy(payload_skeleton)
- job_info_ext = {
- # Asset dependency to wait for at least the scene file to sync.
- "AssetDependency0": data["filepath"],
- }
+ if not self.asset_dependencies:
+ job_info_ext = {}
+
+ else:
+ job_info_ext = {
+ # Asset dependency to wait for at least the scene file to sync.
+ "AssetDependency0": data["filepath"],
+ }
plugin_info = {
"SceneFile": data["filepath"],
diff --git a/pype/plugins/nukestudio/publish/collect_shots.py b/pype/plugins/nukestudio/publish/collect_shots.py
index 7055167143..03fc7ab282 100644
--- a/pype/plugins/nukestudio/publish/collect_shots.py
+++ b/pype/plugins/nukestudio/publish/collect_shots.py
@@ -40,10 +40,10 @@ class CollectShots(api.InstancePlugin):
data["name"] = data["subset"] + "_" + data["asset"]
data["label"] = (
- "{} - {} - tasks: {} - assetbuilds: {} - comments: {}".format(
+ "{} - {} - tasks:{} - assetbuilds:{} - comments:{}".format(
data["asset"],
data["subset"],
- data["tasks"],
+ data["tasks"].keys(),
[x["name"] for x in data.get("assetbuilds", [])],
len(data.get("comments", []))
)
diff --git a/pype/plugins/nukestudio/publish/extract_review_cutup_video.py b/pype/plugins/nukestudio/publish/extract_review_cutup_video.py
index a4fbf90bed..d1ce3675b1 100644
--- a/pype/plugins/nukestudio/publish/extract_review_cutup_video.py
+++ b/pype/plugins/nukestudio/publish/extract_review_cutup_video.py
@@ -76,7 +76,7 @@ class ExtractReviewCutUpVideo(pype.api.Extractor):
# check if audio stream is in input video file
ffprob_cmd = (
- "{ffprobe_path} -i {full_input_path} -show_streams "
+ "{ffprobe_path} -i \"{full_input_path}\" -show_streams "
"-select_streams a -loglevel error"
).format(**locals())
self.log.debug("ffprob_cmd: {}".format(ffprob_cmd))
@@ -106,7 +106,7 @@ class ExtractReviewCutUpVideo(pype.api.Extractor):
# try to get video native resolution data
try:
resolution_output = pype.api.subprocess((
- "{ffprobe_path} -i {full_input_path} -v error "
+ "{ffprobe_path} -i \"{full_input_path}\" -v error "
"-select_streams v:0 -show_entries "
"stream=width,height -of csv=s=x:p=0"
).format(**locals()))
@@ -193,7 +193,7 @@ class ExtractReviewCutUpVideo(pype.api.Extractor):
# append ffmpeg input video clip
input_args.append("-ss {:0.2f}".format(start_sec))
input_args.append("-t {:0.2f}".format(duration_sec))
- input_args.append("-i {}".format(full_input_path))
+ input_args.append("-i \"{}\"".format(full_input_path))
# add copy audio video codec if only shortening clip
if ("_cut-bigger" in tags) and (not empty_add):
@@ -203,8 +203,7 @@ class ExtractReviewCutUpVideo(pype.api.Extractor):
output_args.append("-intra")
# output filename
- output_args.append("-y")
- output_args.append(full_output_path)
+ output_args.append("-y \"{}\"".format(full_output_path))
mov_args = [
ffmpeg_path,
diff --git a/pype/plugins/standalonepublisher/publish/collect_clip_instances.py b/pype/plugins/standalonepublisher/publish/collect_clip_instances.py
index a7af8df143..def0c13a78 100644
--- a/pype/plugins/standalonepublisher/publish/collect_clip_instances.py
+++ b/pype/plugins/standalonepublisher/publish/collect_clip_instances.py
@@ -17,13 +17,13 @@ class CollectClipInstances(pyblish.api.InstancePlugin):
subsets = {
"referenceMain": {
"family": "review",
- "families": ["review", "ftrack"],
+ "families": ["clip", "ftrack"],
# "ftrackFamily": "review",
"extension": ".mp4"
},
"audioMain": {
"family": "audio",
- "families": ["ftrack"],
+ "families": ["clip", "ftrack"],
# "ftrackFamily": "audio",
"extension": ".wav",
# "version": 1
diff --git a/pype/plugins/standalonepublisher/publish/collect_instance_data.py b/pype/plugins/standalonepublisher/publish/collect_instance_data.py
new file mode 100644
index 0000000000..1b32ea9144
--- /dev/null
+++ b/pype/plugins/standalonepublisher/publish/collect_instance_data.py
@@ -0,0 +1,29 @@
+"""
+Requires:
+ Nothing
+
+Provides:
+ Instance
+"""
+
+import pyblish.api
+from pprint import pformat
+
+
+class CollectInstanceData(pyblish.api.InstancePlugin):
+ """
+ Collector with only one reason for its existence - remove 'ftrack'
+ family implicitly added by Standalone Publisher
+ """
+
+ label = "Collect instance data"
+ order = pyblish.api.CollectorOrder + 0.49
+ families = ["render", "plate"]
+ hosts = ["standalonepublisher"]
+
+ def process(self, instance):
+ fps = instance.data["assetEntity"]["data"]["fps"]
+ instance.data.update({
+ "fps": fps
+ })
+ self.log.debug(f"instance.data: {pformat(instance.data)}")
diff --git a/pype/plugins/standalonepublisher/publish/extract_shot_data.py b/pype/plugins/standalonepublisher/publish/extract_shot_data.py
index c39247d6d6..d5af7638ee 100644
--- a/pype/plugins/standalonepublisher/publish/extract_shot_data.py
+++ b/pype/plugins/standalonepublisher/publish/extract_shot_data.py
@@ -10,7 +10,7 @@ class ExtractShotData(pype.api.Extractor):
label = "Extract Shot Data"
hosts = ["standalonepublisher"]
- families = ["review", "audio"]
+ families = ["clip"]
# presets
diff --git a/pype/plugins/standalonepublisher/publish/extract_thumbnail.py b/pype/plugins/standalonepublisher/publish/extract_thumbnail.py
index cddc9c3a82..5882775083 100644
--- a/pype/plugins/standalonepublisher/publish/extract_thumbnail.py
+++ b/pype/plugins/standalonepublisher/publish/extract_thumbnail.py
@@ -64,6 +64,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
else:
# Convert to jpeg if not yet
full_input_path = os.path.join(thumbnail_repre["stagingDir"], file)
+ full_input_path = '"{}"'.format(full_input_path)
self.log.info("input {}".format(full_input_path))
full_thumbnail_path = tempfile.mkstemp(suffix=".jpg")[1]
diff --git a/pype/plugins/standalonepublisher/publish/validate_editorial_resources.py b/pype/plugins/standalonepublisher/publish/validate_editorial_resources.py
index ebc449c4ec..7e1694fbd1 100644
--- a/pype/plugins/standalonepublisher/publish/validate_editorial_resources.py
+++ b/pype/plugins/standalonepublisher/publish/validate_editorial_resources.py
@@ -1,5 +1,3 @@
-import os
-
import pyblish.api
import pype.api
@@ -9,10 +7,14 @@ class ValidateEditorialResources(pyblish.api.InstancePlugin):
label = "Validate Editorial Resources"
hosts = ["standalonepublisher"]
- families = ["audio", "review"]
+ families = ["clip"]
+
order = pype.api.ValidateContentsOrder
def process(self, instance):
+ self.log.debug(
+ f"Instance: {instance}, Families: "
+ f"{[instance.data['family']] + instance.data['families']}")
check_file = instance.data["editorialVideoPath"]
msg = f"Missing \"{check_file}\"."
assert check_file, msg
diff --git a/pype/resources/app_icons/hiero.png b/pype/resources/app_icons/hiero.png
new file mode 100644
index 0000000000..04bbf6265b
Binary files /dev/null and b/pype/resources/app_icons/hiero.png differ
diff --git a/pype/scripts/otio_burnin.py b/pype/scripts/otio_burnin.py
index 156896a759..6607726c73 100644
--- a/pype/scripts/otio_burnin.py
+++ b/pype/scripts/otio_burnin.py
@@ -15,7 +15,7 @@ ffprobe_path = pype.lib.get_ffmpeg_tool_path("ffprobe")
FFMPEG = (
- '{} -loglevel panic -i %(input)s %(filters)s %(args)s%(output)s'
+ '{} -loglevel panic -i "%(input)s" %(filters)s %(args)s%(output)s'
).format(ffmpeg_path)
FFPROBE = (
diff --git a/pype/tools/standalonepublish/widgets/widget_drop_frame.py b/pype/tools/standalonepublish/widgets/widget_drop_frame.py
index e13f701b30..a7abe1b24c 100644
--- a/pype/tools/standalonepublish/widgets/widget_drop_frame.py
+++ b/pype/tools/standalonepublish/widgets/widget_drop_frame.py
@@ -268,9 +268,10 @@ class DropDataFrame(QtWidgets.QFrame):
args = [
ffprobe_path,
'-v', 'quiet',
- '-print_format', 'json',
+ '-print_format json',
'-show_format',
- '-show_streams', filepath
+ '-show_streams',
+ '"{}"'.format(filepath)
]
ffprobe_p = subprocess.Popen(
' '.join(args),
diff --git a/pype/version.py b/pype/version.py
index 96fc614cb2..0f90260218 100644
--- a/pype/version.py
+++ b/pype/version.py
@@ -1 +1 @@
-__version__ = "2.12.1"
+__version__ = "2.12.2"
diff --git a/pype/widgets/message_window.py b/pype/widgets/message_window.py
index 41c709b933..969d6ccdd1 100644
--- a/pype/widgets/message_window.py
+++ b/pype/widgets/message_window.py
@@ -1,4 +1,4 @@
-from Qt import QtWidgets
+from Qt import QtWidgets, QtCore
import sys
import logging
@@ -49,6 +49,17 @@ class Window(QtWidgets.QWidget):
def message(title=None, message=None, level="info", parent=None):
+ """
+ Produces centered dialog with specific level denoting severity
+ Args:
+ title: (string) dialog title
+ message: (string) message
+ level: (string) info|warning|critical
+ parent: (QtWidgets.QApplication)
+
+ Returns:
+ None
+ """
app = parent
if not app:
app = QtWidgets.QApplication(sys.argv)
@@ -68,3 +79,60 @@ def message(title=None, message=None, level="info", parent=None):
# skip all possible issues that may happen feature is not crutial
log.warning("Couldn't center message.", exc_info=True)
# sys.exit(app.exec_())
+
+
+class ScrollMessageBox(QtWidgets.QDialog):
+ """
+ Basic version of scrollable QMessageBox. No other existing dialog
+ implementation is scrollable.
+ Args:
+ icon:
+ title:
+ messages: of messages
+ cancelable: - True if Cancel button should be added
+ """
+ def __init__(self, icon, title, messages, cancelable=False):
+ super(ScrollMessageBox, self).__init__()
+ self.setWindowTitle(title)
+ self.icon = icon
+
+ self.setWindowFlags(QtCore.Qt.WindowTitleHint)
+
+ layout = QtWidgets.QVBoxLayout(self)
+
+ scroll_widget = QtWidgets.QScrollArea(self)
+ scroll_widget.setWidgetResizable(True)
+ content_widget = QtWidgets.QWidget(self)
+ scroll_widget.setWidget(content_widget)
+
+ max_len = 0
+ content_layout = QtWidgets.QVBoxLayout(content_widget)
+ for message in messages:
+ label_widget = QtWidgets.QLabel(message, content_widget)
+ content_layout.addWidget(label_widget)
+ max_len = max(max_len, len(message))
+
+ # guess size of scrollable area
+ max_width = QtWidgets.QApplication.desktop().availableGeometry().width
+ scroll_widget.setMinimumWidth(min(max_width, max_len * 6))
+ layout.addWidget(scroll_widget)
+
+ if not cancelable: # if no specific buttons OK only
+ buttons = QtWidgets.QDialogButtonBox.Ok
+ else:
+ buttons = QtWidgets.QDialogButtonBox.Ok | \
+ QtWidgets.QDialogButtonBox.Cancel
+
+ btn_box = QtWidgets.QDialogButtonBox(buttons)
+ btn_box.accepted.connect(self.accept)
+
+ if cancelable:
+ btn_box.reject.connect(self.reject)
+
+ btn = QtWidgets.QPushButton('Copy to clipboard')
+ btn.clicked.connect(lambda: QtWidgets.QApplication.
+ clipboard().setText("\n".join(messages)))
+ btn_box.addButton(btn, QtWidgets.QDialogButtonBox.NoRole)
+
+ layout.addWidget(btn_box)
+ self.show()
diff --git a/schema/config-1.1.json b/schema/config-1.1.json
new file mode 100644
index 0000000000..ea5ab0ff27
--- /dev/null
+++ b/schema/config-1.1.json
@@ -0,0 +1,87 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+
+ "title": "pype:config-1.1",
+ "description": "A project configuration.",
+
+ "type": "object",
+
+ "additionalProperties": false,
+ "required": [
+ "tasks",
+ "apps"
+ ],
+
+ "properties": {
+ "schema": {
+ "description": "Schema identifier for payload",
+ "type": "string"
+ },
+ "template": {
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ "^.*$": {
+ "type": "string"
+ }
+ }
+ },
+ "tasks": {
+ "type": "object",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string"},
+ "icon": {"type": "string"},
+ "group": {"type": "string"},
+ "label": {"type": "string"}
+ },
+ "required": [
+ "short_name"
+ ]
+ }
+ },
+ "apps": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string"},
+ "icon": {"type": "string"},
+ "group": {"type": "string"},
+ "label": {"type": "string"}
+ },
+ "required": ["name"]
+ }
+ },
+ "families": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string"},
+ "icon": {"type": "string"},
+ "label": {"type": "string"},
+ "hideFilter": {"type": "boolean"}
+ },
+ "required": ["name"]
+ }
+ },
+ "groups": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string"},
+ "icon": {"type": "string"},
+ "color": {"type": "string"},
+ "order": {"type": ["integer", "number"]}
+ },
+ "required": ["name"]
+ }
+ },
+ "copy": {
+ "type": "object"
+ }
+ }
+}
diff --git a/schema/inventory-1.1.json b/schema/inventory-1.1.json
new file mode 100644
index 0000000000..1b572b7d23
--- /dev/null
+++ b/schema/inventory-1.1.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+
+ "title": "pype:config-1.1",
+ "description": "A project configuration.",
+
+ "type": "object",
+
+ "additionalProperties": true
+}
diff --git a/schema/project-2.1.json b/schema/project-2.1.json
new file mode 100644
index 0000000000..40e3bdb638
--- /dev/null
+++ b/schema/project-2.1.json
@@ -0,0 +1,86 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+
+ "title": "pype:project-2.1",
+ "description": "A unit of data",
+
+ "type": "object",
+
+ "additionalProperties": true,
+
+ "required": [
+ "schema",
+ "type",
+ "name",
+ "data",
+ "config"
+ ],
+
+ "properties": {
+ "schema": {
+ "description": "Schema identifier for payload",
+ "type": "string",
+ "enum": ["avalon-core:project-2.1", "pype:project-2.1"],
+ "example": "avalon-core:project-2.1"
+ },
+ "type": {
+ "description": "The type of document",
+ "type": "string",
+ "enum": ["project"],
+ "example": "project"
+ },
+ "parent": {
+ "description": "Unique identifier to parent document",
+ "example": "592c33475f8c1b064c4d1696"
+ },
+ "name": {
+ "description": "Name of directory",
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9_.]*$",
+ "example": "hulk"
+ },
+ "data": {
+ "description": "Document metadata",
+ "type": "object",
+ "example": {
+ "fps": 24,
+ "width": 1920,
+ "height": 1080
+ }
+ },
+ "config": {
+ "type": "object",
+ "description": "Document metadata",
+ "example": {
+ "schema": "pype:config-1.1",
+ "apps": [
+ {
+ "name": "maya2016",
+ "label": "Autodesk Maya 2016"
+ },
+ {
+ "name": "nuke10",
+ "label": "The Foundry Nuke 10.0"
+ }
+ ],
+ "tasks": {
+ "Model": {"short_name": "mdl"},
+ "Render": {"short_name": "rnd"},
+ "Animate": {"short_name": "anim"},
+ "Rig": {"short_name": "rig"},
+ "Lookdev": {"short_name": "look"},
+ "Layout": {"short_name": "lay"}
+ },
+ "template": {
+ "work":
+ "{root}/{project}/{silo}/{asset}/work/{task}/{app}",
+ "publish":
+ "{root}/{project}/{silo}/{asset}/publish/{subset}/v{version:0>3}/{subset}.{representation}"
+ }
+ },
+ "$ref": "config-1.1.json"
+ }
+ },
+
+ "definitions": {}
+}