From 0d402b857cb24d1b0f35a9e1d8d9bb10f868f086 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Jul 2020 18:08:55 +0200 Subject: [PATCH 01/33] ftrack applications registration is based on presets --- .../actions/action_application_loader.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pype/modules/ftrack/actions/action_application_loader.py b/pype/modules/ftrack/actions/action_application_loader.py index ec7fc53fb6..b3166e850e 100644 --- a/pype/modules/ftrack/actions/action_application_loader.py +++ b/pype/modules/ftrack/actions/action_application_loader.py @@ -3,8 +3,7 @@ import toml import time from pype.modules.ftrack.lib import AppAction from avalon import lib -from pype.api import Logger -from pype.lib import get_all_avalon_projects +from pype.api import Logger, config log = Logger().get_logger(__name__) @@ -49,17 +48,26 @@ def registerApp(app, session, plugins_presets): def register(session, plugins_presets={}): - # WARNING getting projects only helps to check connection to mongo - # - without will `discover` of ftrack apps actions take ages - result = get_all_avalon_projects() + app_usages = ( + config.get_presets() + .get("global", {}) + .get("applications") + ) or {} apps = [] - + missing_app_names = [] launchers_path = os.path.join(os.environ["PYPE_CONFIG"], "launchers") for file in os.listdir(launchers_path): filename, ext = os.path.splitext(file) if ext.lower() != ".toml": continue + + app_usage = app_usages.get(filename) + if not app_usage: + if app_usage is None: + missing_app_names.append(filename) + continue + loaded_data = toml.load(os.path.join(launchers_path, file)) app_data = { "name": filename, @@ -67,7 +75,7 @@ def register(session, plugins_presets={}): } apps.append(app_data) - apps = sorted(apps, key=lambda x: x['name']) + apps = sorted(apps, key=lambda app: app["name"]) app_counter = 0 for app in apps: try: @@ -76,7 +84,7 @@ def register(session, plugins_presets={}): time.sleep(0.1) app_counter += 1 except Exception as exc: - log.exception( + log.warning( "\"{}\" - not a proper App ({})".format(app['name'], str(exc)), exc_info=True ) From cedf2920f0962af7dbb3f31e4e6e2bdee7fe61f0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Jul 2020 18:09:09 +0200 Subject: [PATCH 02/33] missing apps are logged out --- .../ftrack/actions/action_application_loader.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pype/modules/ftrack/actions/action_application_loader.py b/pype/modules/ftrack/actions/action_application_loader.py index b3166e850e..ecc5a4fad3 100644 --- a/pype/modules/ftrack/actions/action_application_loader.py +++ b/pype/modules/ftrack/actions/action_application_loader.py @@ -75,6 +75,16 @@ def register(session, plugins_presets={}): } apps.append(app_data) + if missing_app_names: + log.debug( + "Apps not defined in applications usage. ({})".format( + ", ".join(( + "\"{}\"".format(app_name) + for app_name in missing_app_names + )) + ) + ) + apps = sorted(apps, key=lambda app: app["name"]) app_counter = 0 for app in apps: From 15cef44eeabb25df86492176048caa6e5a5fd750 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Jul 2020 18:13:27 +0200 Subject: [PATCH 03/33] method custom_attributes_from_file split into 2 parts --- .../actions/action_create_cust_attrs.py | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index 9845cc8876..19617a7254 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -144,7 +144,7 @@ class CustomAttributes(BaseAction): try: self.prepare_global_data(session) self.avalon_mongo_id_attributes(session, event) - self.custom_attributes_from_file(session, event) + self.custom_attributes_from_file(event) job['status'] = 'done' session.commit() @@ -335,36 +335,40 @@ class CustomAttributes(BaseAction): exc_info=True ) - def custom_attributes_from_file(self, session, event): - presets = config.get_presets()['ftrack']['ftrack_custom_attributes'] - for cust_attr_data in presets: - cust_attr_name = cust_attr_data.get( - 'label', - cust_attr_data.get('key') ) - try: - data = {} - # Get key, label, type - data.update(self.get_required(cust_attr_data)) - # Get hierachical/ entity_type/ object_id - data.update(self.get_entity_type(cust_attr_data)) - # Get group, default, security roles - data.update(self.get_optional(cust_attr_data)) - # Process data - self.process_attribute(data) - except CustAttrException as cae: - if cust_attr_name: - msg = 'Custom attribute error "{}" - {}'.format( - cust_attr_name, str(cae) - ) - else: - msg = 'Custom attribute error - {}'.format(str(cae)) - self.log.warning(msg, exc_info=True) - self.show_message(event, msg) + def custom_attributes_from_file(self, event): + presets = config.get_presets()["ftrack"]["ftrack_custom_attributes"] + for cust_attr_data in presets: + self.process_attr_data(cust_attr_data, event) - return True + def process_attr_data(self, cust_attr_data, event): + cust_attr_name = cust_attr_data.get( + "label", + cust_attr_data.get("key") + ) + + try: + data = {} + # Get key, label, type + data.update(self.get_required(cust_attr_data)) + # Get hierachical/ entity_type/ object_id + data.update(self.get_entity_type(cust_attr_data)) + # Get group, default, security roles + data.update(self.get_optional(cust_attr_data)) + # Process data + self.process_attribute(data) + + except CustAttrException as cae: + if cust_attr_name: + msg = 'Custom attribute error "{}" - {}'.format( + cust_attr_name, str(cae) + ) + else: + msg = 'Custom attribute error - {}'.format(str(cae)) + self.log.warning(msg, exc_info=True) + self.show_message(event, msg) def process_attribute(self, data): existing_atr = self.session.query('CustomAttributeConfiguration').all() From f06c184d5cc5e88340754735181c8f021b78f858 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Jul 2020 18:15:34 +0200 Subject: [PATCH 04/33] added application_definitions method which return app name with labels --- .../actions/action_create_cust_attrs.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index 19617a7254..3378fe5aa3 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -335,8 +335,48 @@ class CustomAttributes(BaseAction): exc_info=True ) + def application_definitions(self): + app_usages = ( + config.get_presets() + .get("global", {}) + .get("applications") + ) or {} + app_definitions = [] + launchers_path = os.path.join(os.environ["PYPE_CONFIG"], "launchers") + + missing_app_names = [] + for file in os.listdir(launchers_path): + app_name, ext = os.path.splitext(file) + if ext.lower() != ".toml": + continue + + if not app_usages.get(app_name): + missing_app_names.append(app_name) + continue + + loaded_data = toml.load(os.path.join(launchers_path, file)) + + ftrack_label = loaded_data.get("ftrack_label") + if ftrack_label: + parts = app_name.split("_") + if len(parts) > 1: + ftrack_label = " ".join((ftrack_label, parts[-1])) + else: + ftrack_label = loaded_data.get("label", app_name) + + app_definitions.append({app_name: ftrack_label}) + + if missing_app_names: + self.log.warning( + "Apps not defined in applications usage. ({})".format( + ", ".join(( + "\"{}\"".format(app_name) + for app_name in missing_app_names + )) + ) ) + return app_definitions def custom_attributes_from_file(self, event): presets = config.get_presets()["ftrack"]["ftrack_custom_attributes"] From f50d46ba3155de1eace4ecf30c14618473104889 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Jul 2020 18:16:36 +0200 Subject: [PATCH 05/33] applications custom attribute is hardcoded in create/upadate action --- .../ftrack/actions/action_create_cust_attrs.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index 3378fe5aa3..b635abb8a7 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -1,4 +1,6 @@ +import os import collections +import toml import json import arrow import ftrack_api @@ -144,6 +146,7 @@ class CustomAttributes(BaseAction): try: self.prepare_global_data(session) self.avalon_mongo_id_attributes(session, event) + self.applications_attribute(event) self.custom_attributes_from_file(event) job['status'] = 'done' @@ -378,6 +381,20 @@ class CustomAttributes(BaseAction): ) return app_definitions + def applications_attribute(self, event): + applications_custom_attr_data = { + "label": "Applications", + "key": "applications", + "type": "enumerator", + "entity_type": "show", + "group": "avalon", + "config": { + "multiselect": True, + "data": self.application_definitions() + } + } + self.process_attr_data(applications_custom_attr_data, event) + def custom_attributes_from_file(self, event): presets = config.get_presets()["ftrack"]["ftrack_custom_attributes"] for cust_attr_data in presets: From a020a661f39e6549940f146ffdc59b040eae12ac Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Jul 2020 18:17:41 +0200 Subject: [PATCH 06/33] make sure applications are not created twice --- pype/modules/ftrack/actions/action_create_cust_attrs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index b635abb8a7..cbc551ac88 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -398,7 +398,9 @@ class CustomAttributes(BaseAction): def custom_attributes_from_file(self, event): presets = config.get_presets()["ftrack"]["ftrack_custom_attributes"] for cust_attr_data in presets: - self.process_attr_data(cust_attr_data, event) + key = applications.get("key") + if key != "applications": + self.process_attr_data(cust_attr_data, event) def process_attr_data(self, cust_attr_data, event): cust_attr_name = cust_attr_data.get( From e167e59001da47310076f80c2d6a84ad137a0eeb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Jul 2020 18:17:56 +0200 Subject: [PATCH 07/33] removed unused fucntion from pype.lib --- pype/lib.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index 87808e53f5..b62840f3a4 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -520,14 +520,6 @@ def set_io_database(): io.install() -def get_all_avalon_projects(): - db = get_avalon_database() - projects = [] - for name in db.collection_names(): - projects.append(db[name].find_one({'type': 'project'})) - return projects - - def filter_pyblish_plugins(plugins): """ This servers as plugin filter / modifier for pyblish. It will load plugin From 01ab07efe6780576ec4daa278a69e864edc21817 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Jul 2020 17:18:19 +0200 Subject: [PATCH 08/33] ftrack custom attributes are defined in pype --- pype/modules/ftrack/lib/avalon_sync.py | 8 +++ .../modules/ftrack/lib/custom_attributes.json | 60 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 pype/modules/ftrack/lib/custom_attributes.json diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index c5c9eb9054..49e4c178c7 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -1,6 +1,7 @@ import os import re import queue +import json import collections import copy @@ -30,6 +31,13 @@ EntitySchemas = { # name of Custom attribute that stores mongo_id from avalon db CustAttrIdKey = "avalon_mongo_id" CustAttrAutoSync = "avalon_auto_sync" +def default_custom_attributes_definition(): + json_file_path = os.path.join( + os.path.dirname(__file__), "custom_attributes.json" + ) + with open(json_file_path, "r") as json_stream: + data = json.load(json_stream) + return data def check_regex(name, entity_type, in_schema=None, schema_patterns=None): diff --git a/pype/modules/ftrack/lib/custom_attributes.json b/pype/modules/ftrack/lib/custom_attributes.json new file mode 100644 index 0000000000..17ff6691d3 --- /dev/null +++ b/pype/modules/ftrack/lib/custom_attributes.json @@ -0,0 +1,60 @@ +{ + "show": { + "avalon_auto_sync": { + "label": "Avalon auto-sync", + "type": "boolean", + "write_security_role": ["API", "Administrator"], + "read_security_role": ["API", "Administrator"] + }, + "library_project": { + "label": "Library Project", + "type": "boolean", + "write_security_role": ["API", "Administrator"], + "read_security_role": ["API", "Administrator"] + } + }, + "is_hierarchical": { + "fps": { + "label": "FPS", + "type": "number", + "config": {"isdecimal": true} + }, + "clipIn": { + "label": "Clip in", + "type": "number" + }, + "clipOut": { + "label": "Clip out", + "type": "number" + }, + "frameStart": { + "label": "Frame start", + "type": "number" + }, + "frameEnd": { + "label": "Frame end", + "type": "number" + }, + "resolutionWidth": { + "label": "Resolution Width", + "type": "number" + }, + "resolutionHeight": { + "label": "Resolution Height", + "type": "number" + }, + "pixelAspect": { + "label": "Pixel aspect", + "type": "number", + "config": {"isdecimal": true} + }, + "handleStart": { + "label": "Frame handles start", + "type": "number" + }, + "handleEnd": { + "label": "Frame handles end", + "type": "number" + } + } +} From 3975c7a0e1aade0f6b17068b5c67b1c4dfd11314 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Jul 2020 17:18:54 +0200 Subject: [PATCH 09/33] added CUST_ATTR_GROUP variable --- pype/modules/ftrack/lib/avalon_sync.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index 49e4c178c7..db0d2e7836 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -28,6 +28,9 @@ EntitySchemas = { "config": "avalon-core:config-1.0" } +# Group name of custom attributes +CUST_ATTR_GROUP = "pype" + # name of Custom attribute that stores mongo_id from avalon db CustAttrIdKey = "avalon_mongo_id" CustAttrAutoSync = "avalon_auto_sync" From fff3c15da0a845443e040f9eef17235ebbb0bf8e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Jul 2020 17:19:48 +0200 Subject: [PATCH 10/33] formatting changes --- pype/modules/ftrack/events/event_sync_to_avalon.py | 4 +++- pype/modules/ftrack/lib/__init__.py | 4 ++-- pype/modules/ftrack/lib/avalon_sync.py | 9 +++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pype/modules/ftrack/events/event_sync_to_avalon.py b/pype/modules/ftrack/events/event_sync_to_avalon.py index 739ec69522..a73c198997 100644 --- a/pype/modules/ftrack/events/event_sync_to_avalon.py +++ b/pype/modules/ftrack/events/event_sync_to_avalon.py @@ -1166,7 +1166,9 @@ class SyncToAvalonEvent(BaseEvent): self.process_session.rolback() # TODO logging # TODO report - error_msg = "Failed to store MongoID to entity's custom attribute" + error_msg = ( + "Failed to store MongoID to entity's custom attribute" + ) report_msg = ( "{}||SyncToAvalon action may solve this issue" ).format(error_msg) diff --git a/pype/modules/ftrack/lib/__init__.py b/pype/modules/ftrack/lib/__init__.py index df546ab725..d8e9c7a11c 100644 --- a/pype/modules/ftrack/lib/__init__.py +++ b/pype/modules/ftrack/lib/__init__.py @@ -5,7 +5,7 @@ from .ftrack_event_handler import BaseEvent from .ftrack_action_handler import BaseAction, statics_icon from .ftrack_app_handler import AppAction -__all__ = [ +__all__ = ( "avalon_sync", "credentials", "BaseHandler", @@ -13,4 +13,4 @@ __all__ = [ "BaseAction", "statics_icon", "AppAction" -] +) diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index db0d2e7836..b3e04efdc3 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -62,10 +62,11 @@ def check_regex(name, entity_type, in_schema=None, schema_patterns=None): if not schema_obj: name_pattern = default_pattern else: - name_pattern = schema_obj.get( - "properties", {}).get( - "name", {}).get( - "pattern", default_pattern + name_pattern = ( + schema_obj + .get("properties", {}) + .get("name", {}) + .get("pattern", default_pattern) ) if schema_patterns is not None: schema_patterns[schema_name] = name_pattern From 8edba507b3f1abd1b17acc554e2a33bff7e7d67a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Jul 2020 17:20:53 +0200 Subject: [PATCH 11/33] renamed custom attributes contatnts and get_avalon_attrs to get_pype_attrs --- .../action_clean_hierarchical_attributes.py | 4 +- .../modules/ftrack/actions/action_delivery.py | 4 +- .../ftrack/actions/action_prepare_project.py | 4 +- .../action_store_thumbnails_to_avalon.py | 4 +- .../events/event_del_avalon_id_from_new.py | 8 ++-- .../ftrack/events/event_sync_to_avalon.py | 30 ++++++------ .../ftrack/events/event_user_assigment.py | 4 +- pype/modules/ftrack/lib/avalon_sync.py | 48 ++++++++++--------- 8 files changed, 54 insertions(+), 52 deletions(-) diff --git a/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py b/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py index 86503ff5bc..e81e587f0a 100644 --- a/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py +++ b/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py @@ -1,7 +1,7 @@ import collections import ftrack_api from pype.modules.ftrack.lib import BaseAction, statics_icon -from pype.modules.ftrack.lib.avalon_sync import get_avalon_attr +from pype.modules.ftrack.lib.avalon_sync import get_pype_attr class CleanHierarchicalAttrsAction(BaseAction): @@ -48,7 +48,7 @@ class CleanHierarchicalAttrsAction(BaseAction): ) entity_ids_joined = ", ".join(all_entities_ids) - attrs, hier_attrs = get_avalon_attr(session) + attrs, hier_attrs = get_pype_attr(session) for attr in hier_attrs: configuration_key = attr["key"] diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index a2048222e5..d4b86d1278 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -11,7 +11,7 @@ from avalon.vendor import filelink from pype.api import Anatomy from pype.modules.ftrack.lib import BaseAction, statics_icon -from pype.modules.ftrack.lib.avalon_sync import CustAttrIdKey +from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY from pype.modules.ftrack.lib.io_nonsingleton import DbConnector @@ -243,7 +243,7 @@ class Delivery(BaseAction): version = entity["version"] parent = asset["parent"] - parent_mongo_id = parent["custom_attributes"].get(CustAttrIdKey) + parent_mongo_id = parent["custom_attributes"].get(CUST_ATTR_ID_KEY) if parent_mongo_id: parent_mongo_id = ObjectId(parent_mongo_id) else: diff --git a/pype/modules/ftrack/actions/action_prepare_project.py b/pype/modules/ftrack/actions/action_prepare_project.py index f51a9eb9a6..b3a2a20151 100644 --- a/pype/modules/ftrack/actions/action_prepare_project.py +++ b/pype/modules/ftrack/actions/action_prepare_project.py @@ -3,7 +3,7 @@ import json from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.api import config, Anatomy, project_overrides_dir_path -from pype.modules.ftrack.lib.avalon_sync import get_avalon_attr +from pype.modules.ftrack.lib.avalon_sync import get_pype_attr class PrepareProject(BaseAction): @@ -221,7 +221,7 @@ class PrepareProject(BaseAction): def _attributes_to_set(self, project_defaults): attributes_to_set = {} - cust_attrs, hier_cust_attrs = get_avalon_attr(self.session, True) + cust_attrs, hier_cust_attrs = get_pype_attr(self.session, True) for attr in hier_cust_attrs: key = attr["key"] diff --git a/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py b/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py index b399dab7ce..94ca503233 100644 --- a/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py +++ b/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py @@ -8,7 +8,7 @@ from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.api import Anatomy from pype.modules.ftrack.lib.io_nonsingleton import DbConnector -from pype.modules.ftrack.lib.avalon_sync import CustAttrIdKey +from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY class StoreThumbnailsToAvalon(BaseAction): @@ -390,7 +390,7 @@ class StoreThumbnailsToAvalon(BaseAction): return output asset_ent = None - asset_mongo_id = parent["custom_attributes"].get(CustAttrIdKey) + asset_mongo_id = parent["custom_attributes"].get(CUST_ATTR_ID_KEY) if asset_mongo_id: try: asset_mongo_id = ObjectId(asset_mongo_id) diff --git a/pype/modules/ftrack/events/event_del_avalon_id_from_new.py b/pype/modules/ftrack/events/event_del_avalon_id_from_new.py index 89bad52f29..47a87e9ba5 100644 --- a/pype/modules/ftrack/events/event_del_avalon_id_from_new.py +++ b/pype/modules/ftrack/events/event_del_avalon_id_from_new.py @@ -1,5 +1,5 @@ from pype.modules.ftrack.lib import BaseEvent -from pype.modules.ftrack.lib.avalon_sync import CustAttrIdKey +from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY from pype.modules.ftrack.events.event_sync_to_avalon import SyncToAvalonEvent @@ -29,7 +29,7 @@ class DelAvalonIdFromNew(BaseEvent): elif ( entity.get('action', None) == 'update' and - CustAttrIdKey in entity['keys'] and + CUST_ATTR_ID_KEY in entity['keys'] and entity_id in created ): ftrack_entity = session.get( @@ -38,11 +38,11 @@ class DelAvalonIdFromNew(BaseEvent): ) cust_attr = ftrack_entity['custom_attributes'][ - CustAttrIdKey + CUST_ATTR_ID_KEY ] if cust_attr != '': - ftrack_entity['custom_attributes'][CustAttrIdKey] = '' + ftrack_entity['custom_attributes'][CUST_ATTR_ID_KEY] = '' session.commit() except Exception: diff --git a/pype/modules/ftrack/events/event_sync_to_avalon.py b/pype/modules/ftrack/events/event_sync_to_avalon.py index a73c198997..efcb74a608 100644 --- a/pype/modules/ftrack/events/event_sync_to_avalon.py +++ b/pype/modules/ftrack/events/event_sync_to_avalon.py @@ -14,7 +14,7 @@ from avalon import schema from pype.modules.ftrack.lib import avalon_sync from pype.modules.ftrack.lib.avalon_sync import ( - CustAttrIdKey, CustAttrAutoSync, EntitySchemas + CUST_ATTR_ID_KEY, CUST_ATTR_AUTO_SYNC, EntitySchemas ) import ftrack_api from pype.modules.ftrack import BaseEvent @@ -103,7 +103,7 @@ class SyncToAvalonEvent(BaseEvent): @property def avalon_cust_attrs(self): if self._avalon_cust_attrs is None: - self._avalon_cust_attrs = avalon_sync.get_avalon_attr( + self._avalon_cust_attrs = avalon_sync.get_pype_attr( self.process_session ) return self._avalon_cust_attrs @@ -220,7 +220,7 @@ class SyncToAvalonEvent(BaseEvent): def avalon_custom_attributes(self): """Return info about changeability of entity and it's parents.""" if self._avalon_custom_attributes is None: - self._avalon_custom_attributes = avalon_sync.get_avalon_attr( + self._avalon_custom_attributes = avalon_sync.get_pype_attr( self.process_session ) return self._avalon_custom_attributes @@ -557,10 +557,10 @@ class SyncToAvalonEvent(BaseEvent): continue changes = ent_info["changes"] - if CustAttrAutoSync not in changes: + if CUST_ATTR_AUTO_SYNC not in changes: continue - auto_sync = changes[CustAttrAutoSync]["new"] + auto_sync = changes[CUST_ATTR_AUTO_SYNC]["new"] if auto_sync == "1": # Trigger sync to avalon action if auto sync was turned on ft_project = self.cur_project @@ -593,16 +593,16 @@ class SyncToAvalonEvent(BaseEvent): ft_project = self.cur_project # Check if auto-sync custom attribute exists - if CustAttrAutoSync not in ft_project["custom_attributes"]: + if CUST_ATTR_AUTO_SYNC not in ft_project["custom_attributes"]: # TODO should we sent message to someone? self.log.error(( "Custom attribute \"{}\" is not created or user \"{}\" used" " for Event server don't have permissions to access it!" - ).format(CustAttrAutoSync, self.session.api_user)) + ).format(CUST_ATTR_AUTO_SYNC, self.session.api_user)) return True # Skip if auto-sync is not set - auto_sync = ft_project["custom_attributes"][CustAttrAutoSync] + auto_sync = ft_project["custom_attributes"][CUST_ATTR_AUTO_SYNC] if auto_sync is not True: return True @@ -844,7 +844,7 @@ class SyncToAvalonEvent(BaseEvent): new_entity["custom_attributes"][key] = val - new_entity["custom_attributes"][CustAttrIdKey] = ( + new_entity["custom_attributes"][CUST_ATTR_ID_KEY] = ( str(avalon_entity["_id"]) ) ent_path = self.get_ent_path(new_entity_id) @@ -1097,7 +1097,7 @@ class SyncToAvalonEvent(BaseEvent): continue final_entity["data"][key] = val - _mongo_id_str = cust_attrs.get(CustAttrIdKey) + _mongo_id_str = cust_attrs.get(CUST_ATTR_ID_KEY) if _mongo_id_str: try: _mongo_id = ObjectId(_mongo_id_str) @@ -1158,8 +1158,8 @@ class SyncToAvalonEvent(BaseEvent): self.log.debug("Entity was synchronized <{}>".format(ent_path)) mongo_id_str = str(mongo_id) - if mongo_id_str != ftrack_ent["custom_attributes"][CustAttrIdKey]: - ftrack_ent["custom_attributes"][CustAttrIdKey] = mongo_id_str + if mongo_id_str != ftrack_ent["custom_attributes"][CUST_ATTR_ID_KEY]: + ftrack_ent["custom_attributes"][CUST_ATTR_ID_KEY] = mongo_id_str try: self.process_session.commit() except Exception: @@ -1247,7 +1247,7 @@ class SyncToAvalonEvent(BaseEvent): self.process_session, entity, hier_keys, defaults ) for key, val in hier_values.items(): - if key == CustAttrIdKey: + if key == CUST_ATTR_ID_KEY: continue output[key] = val @@ -1689,7 +1689,7 @@ class SyncToAvalonEvent(BaseEvent): if "_hierarchical" not in temp_dict: hier_mongo_id_configuration_id = None for attr in hier_attrs: - if attr["key"] == CustAttrIdKey: + if attr["key"] == CUST_ATTR_ID_KEY: hier_mongo_id_configuration_id = attr["id"] break temp_dict["_hierarchical"] = hier_mongo_id_configuration_id @@ -1706,7 +1706,7 @@ class SyncToAvalonEvent(BaseEvent): for attr in cust_attrs: key = attr["key"] - if key != CustAttrIdKey: + if key != CUST_ATTR_ID_KEY: continue if attr["entity_type"] != ent_info["entityType"]: diff --git a/pype/modules/ftrack/events/event_user_assigment.py b/pype/modules/ftrack/events/event_user_assigment.py index e198ced618..d1b3439c8f 100644 --- a/pype/modules/ftrack/events/event_user_assigment.py +++ b/pype/modules/ftrack/events/event_user_assigment.py @@ -3,7 +3,7 @@ import re import subprocess from pype.modules.ftrack import BaseEvent -from pype.modules.ftrack.lib.avalon_sync import CustAttrIdKey +from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY from pype.modules.ftrack.lib.io_nonsingleton import DbConnector from bson.objectid import ObjectId @@ -106,7 +106,7 @@ class UserAssigmentEvent(BaseEvent): self.db_con.Session['AVALON_PROJECT'] = task['project']['full_name'] avalon_entity = None - parent_id = parent['custom_attributes'].get(CustAttrIdKey) + parent_id = parent['custom_attributes'].get(CUST_ATTR_ID_KEY) if parent_id: parent_id = ObjectId(parent_id) avalon_entity = self.db_con.find_one({ diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index b3e04efdc3..07a1b17770 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -32,8 +32,10 @@ EntitySchemas = { CUST_ATTR_GROUP = "pype" # name of Custom attribute that stores mongo_id from avalon db -CustAttrIdKey = "avalon_mongo_id" -CustAttrAutoSync = "avalon_auto_sync" +CUST_ATTR_ID_KEY = "avalon_mongo_id" +CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" + + def default_custom_attributes_definition(): json_file_path = os.path.join( os.path.dirname(__file__), "custom_attributes.json" @@ -76,14 +78,14 @@ def check_regex(name, entity_type, in_schema=None, schema_patterns=None): return False -def get_avalon_attr(session, split_hierarchical=True): +def get_pype_attr(session, split_hierarchical=True): custom_attributes = [] hier_custom_attributes = [] cust_attrs_query = ( "select id, entity_type, object_type_id, is_hierarchical, default" " from CustomAttributeConfiguration" - " where group.name = \"avalon\"" - ) + " where group.name = \"{}\"" + ).format(CUST_ATTR_GROUP) all_avalon_attr = session.query(cust_attrs_query).all() for cust_attr in all_avalon_attr: if split_hierarchical and cust_attr["is_hierarchical"]: @@ -334,12 +336,12 @@ class SyncEntitiesFactory: "*** Synchronization initialization started <{}>." ).format(project_full_name)) # Check if `avalon_mongo_id` custom attribute exist or is accessible - if CustAttrIdKey not in ft_project["custom_attributes"]: + if CUST_ATTR_ID_KEY not in ft_project["custom_attributes"]: items = [] items.append({ "type": "label", "value": "# Can't access Custom attribute <{}>".format( - CustAttrIdKey + CUST_ATTR_ID_KEY ) }) items.append({ @@ -699,7 +701,7 @@ class SyncEntitiesFactory: def set_cutom_attributes(self): self.log.debug("* Preparing custom attributes") # Get custom attributes and values - custom_attrs, hier_attrs = get_avalon_attr(self.session) + custom_attrs, hier_attrs = get_pype_attr(self.session) ent_types = self.session.query("select id, name from ObjectType").all() ent_types_by_name = { ent_type["name"]: ent_type["id"] for ent_type in ent_types @@ -916,7 +918,7 @@ class SyncEntitiesFactory: project_values[key] = value for key in avalon_hier: - if key == CustAttrIdKey: + if key == CUST_ATTR_ID_KEY: continue value = self.entities_dict[top_id]["avalon_attrs"][key] if value is not None: @@ -1070,7 +1072,7 @@ class SyncEntitiesFactory: same_mongo_id = [] all_mongo_ids = {} for ftrack_id, entity_dict in self.entities_dict.items(): - mongo_id = entity_dict["avalon_attrs"].get(CustAttrIdKey) + mongo_id = entity_dict["avalon_attrs"].get(CUST_ATTR_ID_KEY) if not mongo_id: continue if mongo_id in all_mongo_ids: @@ -1101,7 +1103,7 @@ class SyncEntitiesFactory: entity_dict = self.entities_dict[ftrack_id] ent_path = self.get_ent_path(ftrack_id) - mongo_id = entity_dict["avalon_attrs"].get(CustAttrIdKey) + mongo_id = entity_dict["avalon_attrs"].get(CUST_ATTR_ID_KEY) av_ent_by_mongo_id = self.avalon_ents_by_id.get(mongo_id) if av_ent_by_mongo_id: av_ent_ftrack_id = av_ent_by_mongo_id.get("data", {}).get( @@ -1122,7 +1124,7 @@ class SyncEntitiesFactory: continue _entity_dict = self.entities_dict[_ftrack_id] - _mongo_id = _entity_dict["avalon_attrs"][CustAttrIdKey] + _mongo_id = _entity_dict["avalon_attrs"][CUST_ATTR_ID_KEY] _av_ent_by_mongo_id = self.avalon_ents_by_id.get( _mongo_id ) @@ -1515,11 +1517,11 @@ class SyncEntitiesFactory: avalon_attrs = self.entities_dict[ftrack_id]["avalon_attrs"] if ( - CustAttrIdKey not in avalon_attrs or - avalon_attrs[CustAttrIdKey] != avalon_id + CUST_ATTR_ID_KEY not in avalon_attrs or + avalon_attrs[CUST_ATTR_ID_KEY] != avalon_id ): configuration_id = self.entities_dict[ftrack_id][ - "avalon_attrs_id"][CustAttrIdKey] + "avalon_attrs_id"][CUST_ATTR_ID_KEY] _entity_key = collections.OrderedDict({ "configuration_id": configuration_id, @@ -1599,7 +1601,7 @@ class SyncEntitiesFactory: # avalon_archived_by_id avalon_archived_by_name current_id = ( - entity_dict["avalon_attrs"].get(CustAttrIdKey) or "" + entity_dict["avalon_attrs"].get(CUST_ATTR_ID_KEY) or "" ).strip() mongo_id = current_id name = entity_dict["name"] @@ -1635,14 +1637,14 @@ class SyncEntitiesFactory: if current_id != new_id_str: # store mongo id to ftrack entity configuration_id = self.hier_cust_attr_ids_by_key.get( - CustAttrIdKey + CUST_ATTR_ID_KEY ) if not configuration_id: - # NOTE this is for cases when CustAttrIdKey key is not + # NOTE this is for cases when CUST_ATTR_ID_KEY key is not # hierarchical custom attribute but per entity type configuration_id = self.entities_dict[ftrack_id][ "avalon_attrs_id" - ][CustAttrIdKey] + ][CUST_ATTR_ID_KEY] _entity_key = collections.OrderedDict({ "configuration_id": configuration_id, @@ -1751,7 +1753,7 @@ class SyncEntitiesFactory: project_item = self.entities_dict[self.ft_project_id]["final_entity"] mongo_id = ( self.entities_dict[self.ft_project_id]["avalon_attrs"].get( - CustAttrIdKey + CUST_ATTR_ID_KEY ) or "" ).strip() @@ -1782,7 +1784,7 @@ class SyncEntitiesFactory: # store mongo id to ftrack entity entity = self.entities_dict[self.ft_project_id]["entity"] - entity["custom_attributes"][CustAttrIdKey] = str(new_id) + entity["custom_attributes"][CUST_ATTR_ID_KEY] = str(new_id) def _bubble_changeability(self, unchangeable_ids): unchangeable_queue = queue.Queue() @@ -2163,7 +2165,7 @@ class SyncEntitiesFactory: if new_entity_id not in p_chilren: self.entities_dict[parent_id]["children"].append(new_entity_id) - cust_attr, hier_attrs = get_avalon_attr(self.session) + cust_attr, hier_attrs = get_pype_attr(self.session) for _attr in cust_attr: key = _attr["key"] if key not in av_entity["data"]: @@ -2179,7 +2181,7 @@ class SyncEntitiesFactory: new_entity["custom_attributes"][key] = value av_entity_id = str(av_entity["_id"]) - new_entity["custom_attributes"][CustAttrIdKey] = av_entity_id + new_entity["custom_attributes"][CUST_ATTR_ID_KEY] = av_entity_id self.ftrack_avalon_mapper[new_entity_id] = av_entity_id self.avalon_ftrack_mapper[av_entity_id] = new_entity_id From e756dd60ec85f2bc8aff620349be28335c38842f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Jul 2020 17:21:55 +0200 Subject: [PATCH 12/33] intent and tools attributes are based on global presets --- .../actions/action_create_cust_attrs.py | 55 +++++++++++++++++++ pype/tools/pyblish_pype/model.py | 5 +- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index cbc551ac88..a81f42f74c 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -395,6 +395,61 @@ class CustomAttributes(BaseAction): } self.process_attr_data(applications_custom_attr_data, event) + def tools_attribute(self, event): + tool_usages = self.presets.get("global", {}).get("tools") or {} + tools_data = [] + for tool_name, usage in tool_usages.items(): + if usage: + tools_data.append({tool_name: tool_name}) + + tools_custom_attr_data = { + "label": "Tools", + "key": "tools_env", + "type": "enumerator", + "is_hierarchical": True, + "group": CUST_ATTR_GROUP, + "config": { + "multiselect": True, + "data": tools_data + } + } + self.process_attr_data(tools_custom_attr_data, event) + + def intent_attribute(self, event): + intent_key_values = ( + self.presets + .get("global", {}) + .get("intent", {}) + .get("items", {}) + ) or {} + + intent_values = [] + for key, label in intent_key_values.items(): + if not key or not label: + self.log.info(( + "Skipping intent row: {{\"{}\": \"{}\"}}" + " because of empty key or label." + ).format(key, label)) + continue + + intent_values.append({key: label}) + + if not intent_values: + return + + intent_custom_attr_data = { + "label": "Intent", + "key": "intent", + "type": "enumerator", + "entity_type": "assetversion", + "group": CUST_ATTR_GROUP, + "config": { + "multiselect": False, + "data": intent_values + } + } + self.process_attr_data(intent_custom_attr_data, event) + def custom_attributes_from_file(self, event): presets = config.get_presets()["ftrack"]["ftrack_custom_attributes"] for cust_attr_data in presets: diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index 9086003258..b1f953bbe4 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -105,11 +105,10 @@ class IntentModel(QtGui.QStandardItemModel): intents_preset = ( config.get_presets() - .get("tools", {}) - .get("pyblish", {}) - .get("ui", {}) + .get("global", {}) .get("intents", {}) ) + default = intents_preset.get("default") items = intents_preset.get("items", {}) if not items: From 6e6b38e4b19b493e1830a2ff653d4cb033b4f761 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Jul 2020 17:22:50 +0200 Subject: [PATCH 13/33] few fixes and only roles and default values can be set via presets --- .../actions/action_create_cust_attrs.py | 191 ++++++++++-------- 1 file changed, 106 insertions(+), 85 deletions(-) diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index a81f42f74c..f3bba1a9b4 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -5,7 +5,9 @@ import json import arrow import ftrack_api from pype.modules.ftrack.lib import BaseAction, statics_icon -from pype.modules.ftrack.lib.avalon_sync import CustAttrIdKey +from pype.modules.ftrack.lib.avalon_sync import ( + CUST_ATTR_ID_KEY, CUST_ATTR_GROUP, default_custom_attributes_definition +) from pype.api import config """ @@ -117,11 +119,15 @@ class CustomAttributes(BaseAction): role_list = ['Pypeclub', 'Administrator'] icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") - required_keys = ['key', 'label', 'type'] - type_posibilities = [ - 'text', 'boolean', 'date', 'enumerator', - 'dynamic enumerator', 'number' - ] + required_keys = ("key", "label", "type") + + presetable_keys = ("default", "write_security_role", "read_security_role") + hierarchical_key = "is_hierarchical" + + type_posibilities = ( + "text", "boolean", "date", "enumerator", + "dynamic enumerator", "number" + ) def discover(self, session, entities, event): ''' @@ -143,22 +149,24 @@ class CustomAttributes(BaseAction): }) }) session.commit() + try: self.prepare_global_data(session) self.avalon_mongo_id_attributes(session, event) self.applications_attribute(event) + self.tools_attribute(event) + self.intent_attribute(event) self.custom_attributes_from_file(event) job['status'] = 'done' session.commit() - except Exception as exc: + except Exception: session.rollback() - job['status'] = 'failed' + job["status"] = "failed" session.commit() self.log.error( - 'Creating custom attributes failed ({})'.format(exc), - exc_info=True + "Creating custom attributes failed ({})", exc_info=True ) return True @@ -185,20 +193,17 @@ class CustomAttributes(BaseAction): self.groups = {} + self.presets = config.get_presets() + self.attrs_presets = ( + self.presets.get("ftrack", {}).get("ftrack_custom_attributes") + ) or {} + def avalon_mongo_id_attributes(self, session, event): + self.create_hierarchical_mongo_attr(session, event) + hierarchical_attr, object_type_attrs = ( self.mongo_id_custom_attributes(session) ) - - if hierarchical_attr is None: - self.create_hierarchical_mongo_attr(session) - hierarchical_attr, object_type_attrs = ( - self.mongo_id_custom_attributes(session) - ) - - if hierarchical_attr is None: - return - if object_type_attrs: self.convert_mongo_id_to_hierarchical( hierarchical_attr, object_type_attrs, session, event @@ -209,7 +214,7 @@ class CustomAttributes(BaseAction): "select id, entity_type, object_type_id, is_hierarchical, default" " from CustomAttributeConfiguration" " where key = \"{}\"" - ).format(CustAttrIdKey) + ).format(CUST_ATTR_ID_KEY) mongo_id_avalon_attr = session.query(cust_attrs_query).all() heirarchical_attr = None @@ -223,32 +228,22 @@ class CustomAttributes(BaseAction): return heirarchical_attr, object_type_attrs - def create_hierarchical_mongo_attr(self, session): - # Attribute Name and Label - cust_attr_label = "Avalon/Mongo ID" - + def create_hierarchical_mongo_attr(self, session, event): # Set security roles for attribute - role_list = ("API", "Administrator", "Pypeclub") - roles = self.get_security_roles(role_list) - # Set Text type of Attribute - custom_attribute_type = self.types_per_name["text"] - # Set group to 'avalon' - group = self.get_group("avalon") - + default_role_list = ("API", "Administrator", "Pypeclub") data = { - "key": CustAttrIdKey, - "label": cust_attr_label, - "type": custom_attribute_type, + "key": CUST_ATTR_ID_KEY, + "label": "Avalon/Mongo ID", + "type": "text", "default": "", - "write_security_roles": roles, - "read_security_roles": roles, - "group": group, + "write_security_roles": default_role_list, + "read_security_roles": default_role_list, + "group": CUST_ATTR_GROUP, "is_hierarchical": True, - "entity_type": "show", - "config": json.dumps({"markdown": False}) + "config": {"markdown": False} } - self.process_attribute(data) + self.process_attr_data(data, event) def convert_mongo_id_to_hierarchical( self, hierarchical_attr, object_type_attrs, session, event @@ -339,11 +334,7 @@ class CustomAttributes(BaseAction): ) def application_definitions(self): - app_usages = ( - config.get_presets() - .get("global", {}) - .get("applications") - ) or {} + app_usages = self.presets.get("global", {}).get("applications") or {} app_definitions = [] launchers_path = os.path.join(os.environ["PYPE_CONFIG"], "launchers") @@ -387,7 +378,7 @@ class CustomAttributes(BaseAction): "key": "applications", "type": "enumerator", "entity_type": "show", - "group": "avalon", + "group": CUST_ATTR_GROUP, "config": { "multiselect": True, "data": self.application_definitions() @@ -451,17 +442,43 @@ class CustomAttributes(BaseAction): self.process_attr_data(intent_custom_attr_data, event) def custom_attributes_from_file(self, event): - presets = config.get_presets()["ftrack"]["ftrack_custom_attributes"] - for cust_attr_data in presets: - key = applications.get("key") - if key != "applications": - self.process_attr_data(cust_attr_data, event) + # Load json with custom attributes configurations + cust_attr_def = default_custom_attributes_definition() + attrs_data = [] + + # Prepare data of hierarchical attributes + hierarchical_attrs = cust_attr_def.pop(self.hierarchical_key, {}) + for key, cust_attr_data in hierarchical_attrs.items(): + cust_attr_data["key"] = key + cust_attr_data["is_hierarchical"] = True + attrs_data.append(cust_attr_data) + + # Prepare data of entity specific attributes + for entity_type, cust_attr_datas in cust_attr_def.items(): + for key, cust_attr_data in cust_attr_datas.items(): + cust_attr_data["key"] = key + cust_attr_data["entity_type"] = entity_type + attrs_data.append(cust_attr_data) + + # Process prepared data + for cust_attr_data in attrs_data: + # Add group + cust_attr_data["group"] = CUST_ATTR_GROUP + self.process_attr_data(cust_attr_data, event) def process_attr_data(self, cust_attr_data, event): - cust_attr_name = cust_attr_data.get( - "label", - cust_attr_data.get("key") - ) + attr_key = cust_attr_data["key"] + if cust_attr_data.get("is_hierarchical"): + entity_key = self.hierarchical_key + else: + entity_key = cust_attr_data["entity_type"] + + entity_presets = self.attrs_presets.get(entity_key) or {} + key_presets = entity_presets.get(attr_key) or {} + + for key, value in key_presets.items(): + if key in self.presetable_keys and value: + cust_attr_data[key] = value try: data = {} @@ -475,6 +492,8 @@ class CustomAttributes(BaseAction): self.process_attribute(data) except CustAttrException as cae: + cust_attr_name = cust_attr_data.get("label", attr_key) + if cust_attr_name: msg = 'Custom attribute error "{}" - {}'.format( cust_attr_name, str(cae) @@ -485,59 +504,61 @@ class CustomAttributes(BaseAction): self.show_message(event, msg) def process_attribute(self, data): - existing_atr = self.session.query('CustomAttributeConfiguration').all() + existing_attrs = self.session.query( + "CustomAttributeConfiguration" + ).all() matching = [] - for attr in existing_atr: + for attr in existing_attrs: if ( - attr['key'] != data['key'] or - attr['type']['name'] != data['type']['name'] + attr["key"] != data["key"] or + attr["type"]["name"] != data["type"]["name"] ): continue - if data.get('is_hierarchical', False) is True: - if attr['is_hierarchical'] is True: + if data.get("is_hierarchical") is True: + if attr["is_hierarchical"] is True: matching.append(attr) - elif 'object_type_id' in data: + elif "object_type_id" in data: if ( - attr['entity_type'] == data['entity_type'] and - attr['object_type_id'] == data['object_type_id'] + attr["entity_type"] == data["entity_type"] and + attr["object_type_id"] == data["object_type_id"] ): matching.append(attr) else: - if attr['entity_type'] == data['entity_type']: + if attr["entity_type"] == data["entity_type"]: matching.append(attr) if len(matching) == 0: - self.session.create('CustomAttributeConfiguration', data) + self.session.create("CustomAttributeConfiguration", data) self.session.commit() self.log.debug( - '{}: "{}" created'.format(self.label, data['label']) + "Custom attribute \"{}\" created".format(data["label"]) ) elif len(matching) == 1: attr_update = matching[0] for key in data: - if ( - key not in [ - 'is_hierarchical', 'entity_type', 'object_type_id' - ] + if key not in ( + "is_hierarchical", "entity_type", "object_type_id" ): attr_update[key] = data[key] - self.log.debug( - '{}: "{}" updated'.format(self.label, data['label']) - ) self.session.commit() + self.log.debug( + "Custom attribute \"{}\" updated".format(data["label"]) + ) else: - raise CustAttrException('Is duplicated') + raise CustAttrException(( + "Custom attribute is duplicated. Key: \"{}\" Type: \"{}\"" + ).format(data["key"], data["type"]["name"])) def get_required(self, attr): output = {} for key in self.required_keys: if key not in attr: raise CustAttrException( - 'Key {} is required - please set'.format(key) + "BUG: Key \"{}\" is required".format(key) ) if attr['type'].lower() not in self.type_posibilities: @@ -711,17 +732,17 @@ class CustomAttributes(BaseAction): def get_optional(self, attr): output = {} - if 'group' in attr: - output['group'] = self.get_group(attr) - if 'default' in attr: - output['default'] = self.get_default(attr) + if "group" in attr: + output["group"] = self.get_group(attr) + if "default" in attr: + output["default"] = self.get_default(attr) roles_read = [] roles_write = [] - if 'read_security_roles' in output: - roles_read = attr['read_security_roles'] - if 'read_security_roles' in output: - roles_write = attr['write_security_roles'] + if "read_security_roles" in attr: + roles_read = attr["read_security_roles"] + if "write_security_roles" in attr: + roles_write = attr["write_security_roles"] output['read_security_roles'] = self.get_security_roles(roles_read) output['write_security_roles'] = self.get_security_roles(roles_write) From aea8f16437803928e65012fd17d78ce936b7aa37 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jul 2020 15:35:26 +0200 Subject: [PATCH 14/33] fix intent key --- pype/tools/pyblish_pype/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index b1f953bbe4..b7b300f154 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -106,7 +106,7 @@ class IntentModel(QtGui.QStandardItemModel): intents_preset = ( config.get_presets() .get("global", {}) - .get("intents", {}) + .get("intent", {}) ) default = intents_preset.get("default") From 11151b412244b7d6b6a575612d694b159f249f34 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jul 2020 16:24:38 +0200 Subject: [PATCH 15/33] added possibility to add task entity type custom attributes --- .../ftrack/actions/action_create_cust_attrs.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index f3bba1a9b4..593da28cb0 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -455,10 +455,20 @@ class CustomAttributes(BaseAction): # Prepare data of entity specific attributes for entity_type, cust_attr_datas in cust_attr_def.items(): - for key, cust_attr_data in cust_attr_datas.items(): - cust_attr_data["key"] = key - cust_attr_data["entity_type"] = entity_type - attrs_data.append(cust_attr_data) + if entity_type.lower() != "task": + for key, cust_attr_data in cust_attr_datas.items(): + cust_attr_data["key"] = key + cust_attr_data["entity_type"] = entity_type + attrs_data.append(cust_attr_data) + continue + + # Task should have nested level for object type + for object_type, _cust_attr_datas in cust_attr_datas.items(): + for key, cust_attr_data in _cust_attr_datas.items(): + cust_attr_data["key"] = key + cust_attr_data["entity_type"] = entity_type + cust_attr_data["object_type"] = object_type + attrs_data.append(cust_attr_data) # Process prepared data for cust_attr_data in attrs_data: From c59c589d38e05ccd6c84a70107bead9ebb63b023 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jul 2020 16:26:48 +0200 Subject: [PATCH 16/33] modified docstring for custom attributes --- .../actions/action_create_cust_attrs.py | 143 ++++++++++-------- 1 file changed, 80 insertions(+), 63 deletions(-) diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index 593da28cb0..8b23fc4749 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -12,92 +12,109 @@ from pype.api import config """ This action creates/updates custom attributes. -- first part take care about avalon_mongo_id attribute -- second part is based on json file in templates: - ~/PYPE-TEMPLATES/presets/ftrack/ftrack_custom_attributes.json - - you can add Custom attributes based on these conditions +## First part take care about special attributes + - `avalon_mongo_id` for storing Avalon MongoID + - `applications` based on applications usages + - `tools` based on tools usages + +## Second part is based on json file in ftrack module. +File location: `~/pype/pype/modules/ftrack/ftrack_custom_attributes.json` + +Data in json file is nested dictionary. Keys in first dictionary level +represents Ftrack entity type (task, show, assetversion, user, list, asset) +and dictionary value define attribute. + +There is special key for hierchical attributes `is_hierarchical`. + +Entity types `task` requires to define task object type (Folder, Shot, +Sequence, Task, Library, Milestone, Episode, Asset Build, etc.) at second +dictionary level, task's attributes are nested more. + +*** Not Changeable ********************************************************* + +group (string) + - name of group + - based on attribute `pype.modules.ftrack.lib.CUST_ATTR_GROUP` + - "pype" by default *** Required *************************************************************** label (string) - - label that will show in ftrack + - label that will show in ftrack key (string) - - must contain only chars [a-z0-9_] + - must contain only chars [a-z0-9_] type (string) - - type of custom attribute - - possibilities: text, boolean, date, enumerator, dynamic enumerator, number + - type of custom attribute + - possibilities: + text, boolean, date, enumerator, dynamic enumerator, number *** Required with conditions *********************************************** -entity_type (string) - - if 'is_hierarchical' is set to False - - type of entity - - possibilities: task, show, assetversion, user, list, asset - config (dictionary) - - for each entity type different requirements and possibilities: - - enumerator: multiSelect = True/False(default: False) - data = {key_1:value_1,key_2:value_2,..,key_n:value_n} - - 'data' is Required value with enumerator - - 'key' must contain only chars [a-z0-9_] + - for each attribute type different requirements and possibilities: + - enumerator: + multiSelect = True/False(default: False) + data = {key_1:value_1,key_2:value_2,..,key_n:value_n} + - 'data' is Required value with enumerator + - 'key' must contain only chars [a-z0-9_] - - number: isdecimal = True/False(default: False) + - number: + isdecimal = True/False(default: False) - - text: markdown = True/False(default: False) + - text: + markdown = True/False(default: False) -object_type (string) - - IF ENTITY_TYPE is set to 'task' - - default possibilities: Folder, Shot, Sequence, Task, Library, - Milestone, Episode, Asset Build,... - -*** Optional *************************************************************** +*** Presetable keys ********************************************************** write_security_roles/read_security_roles (array of strings) - - default: ["ALL"] - - strings should be role names (e.g.: ["API", "Administrator"]) - - if set to ["ALL"] - all roles will be availabled - - if first is 'except' - roles will be set to all except roles in array - - Warning: Be carefull with except - roles can be different by company - - example: - write_security_roles = ["except", "User"] - read_security_roles = ["ALL"] - - User is unable to write but can read - -group (string) - - default: None - - name of group + - default: ["ALL"] + - strings should be role names (e.g.: ["API", "Administrator"]) + - if set to ["ALL"] - all roles will be availabled + - if first is 'except' - roles will be set to all except roles in array + - Warning: Be carefull with except - roles can be different by company + - example: + write_security_roles = ["except", "User"] + read_security_roles = ["ALL"] # (User is can only read) default - - default: None - - sets default value for custom attribute: - - text -> string - - number -> integer - - enumerator -> array with string of key/s - - boolean -> bool true/false - - date -> string in format: 'YYYY.MM.DD' or 'YYYY.MM.DD HH:mm:ss' - - example: "2018.12.24" / "2018.1.1 6:0:0" - - dynamic enumerator -> DON'T HAVE DEFAULT VALUE!!! + - default: None + - sets default value for custom attribute: + - text -> string + - number -> integer + - enumerator -> array with string of key/s + - boolean -> bool true/false + - date -> string in format: 'YYYY.MM.DD' or 'YYYY.MM.DD HH:mm:ss' + - example: "2018.12.24" / "2018.1.1 6:0:0" + - dynamic enumerator -> DON'T HAVE DEFAULT VALUE!!! -is_hierarchical (bool) - - default: False - - will set hierachical attribute - - False by default - -EXAMPLE: -{ +Example: +``` +"show": { "avalon_auto_sync": { - "label": "Avalon auto-sync", - "key": "avalon_auto_sync", - "type": "boolean", - "entity_type": "show", - "group": "avalon", - "default": false, - "write_security_role": ["API","Administrator"], - "read_security_role": ["API","Administrator"] + "label": "Avalon auto-sync", + "type": "boolean", + "write_security_role": ["API", "Administrator"], + "read_security_role": ["API", "Administrator"] + } +}, +"is_hierarchical": { + "fps": { + "label": "FPS", + "type": "number", + "config": {"isdecimal": true} + } +}, +"task": { + "library": { + "my_attr_name": { + "label": "My Attr", + "type": "number" + } } } +``` """ From c76e5c9acc59147d760e5993b4eaf9b59b04361a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jul 2020 16:38:11 +0200 Subject: [PATCH 17/33] task entity type can be presetable --- .../actions/action_create_cust_attrs.py | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index 8b23fc4749..0c7e311377 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -211,9 +211,31 @@ class CustomAttributes(BaseAction): self.groups = {} self.presets = config.get_presets() - self.attrs_presets = ( + self.attrs_presets = self.prepare_attribute_pressets() + + def prepare_attribute_pressets(self): + output = {} + + attr_presets = ( self.presets.get("ftrack", {}).get("ftrack_custom_attributes") ) or {} + for entity_type, preset in attr_presets.items(): + # Lower entity type + entity_type = entity_type.lower() + # Just store if entity type is not "task" + if entity_type != "task": + output[entity_type] = preset + continue + + # Prepare empty dictionary for entity type if not set yet + if entity_type not in output: + output[entity_type] = {} + + # Store presets per lowered object type + for obj_type, _preset in preset.items(): + output[entity_type][obj_type.lower()] = _preset + + return output def avalon_mongo_id_attributes(self, session, event): self.create_hierarchical_mongo_attr(session, event) @@ -493,19 +515,30 @@ class CustomAttributes(BaseAction): cust_attr_data["group"] = CUST_ATTR_GROUP self.process_attr_data(cust_attr_data, event) - def process_attr_data(self, cust_attr_data, event): - attr_key = cust_attr_data["key"] - if cust_attr_data.get("is_hierarchical"): + def presets_for_attr_data(self, attr_data): + output = {} + + attr_key = attr_data["key"] + if attr_data.get("is_hierarchical"): entity_key = self.hierarchical_key else: - entity_key = cust_attr_data["entity_type"] + entity_key = attr_data["entity_type"] entity_presets = self.attrs_presets.get(entity_key) or {} + if entity_key.lower() == "task": + object_type = attr_data["object_type"] + entity_presets = entity_presets.get(object_type.lower()) or {} + key_presets = entity_presets.get(attr_key) or {} for key, value in key_presets.items(): if key in self.presetable_keys and value: - cust_attr_data[key] = value + output[key] = value + return output + + def process_attr_data(self, cust_attr_data, event): + attr_presets = self.presets_for_attr_data(cust_attr_data) + cust_attr_data.update(attr_presets) try: data = {} @@ -519,7 +552,7 @@ class CustomAttributes(BaseAction): self.process_attribute(data) except CustAttrException as cae: - cust_attr_name = cust_attr_data.get("label", attr_key) + cust_attr_name = cust_attr_data.get("label", cust_attr_data["key"]) if cust_attr_name: msg = 'Custom attribute error "{}" - {}'.format( From c136e5cb086fc7daabe77f3ab60268559a83ab78 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jul 2020 17:26:15 +0200 Subject: [PATCH 18/33] hound fixes --- .../ftrack/events/event_del_avalon_id_from_new.py | 9 +++------ pype/modules/ftrack/lib/avalon_sync.py | 4 +++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pype/modules/ftrack/events/event_del_avalon_id_from_new.py b/pype/modules/ftrack/events/event_del_avalon_id_from_new.py index 47a87e9ba5..ee82c9589d 100644 --- a/pype/modules/ftrack/events/event_del_avalon_id_from_new.py +++ b/pype/modules/ftrack/events/event_del_avalon_id_from_new.py @@ -37,12 +37,9 @@ class DelAvalonIdFromNew(BaseEvent): entity_id ) - cust_attr = ftrack_entity['custom_attributes'][ - CUST_ATTR_ID_KEY - ] - - if cust_attr != '': - ftrack_entity['custom_attributes'][CUST_ATTR_ID_KEY] = '' + cust_attrs = ftrack_entity["custom_attributes"] + if cust_attrs[CUST_ATTR_ID_KEY]: + cust_attrs[CUST_ATTR_ID_KEY] = "" session.commit() except Exception: diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index 07a1b17770..f78e617f3a 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -1124,7 +1124,9 @@ class SyncEntitiesFactory: continue _entity_dict = self.entities_dict[_ftrack_id] - _mongo_id = _entity_dict["avalon_attrs"][CUST_ATTR_ID_KEY] + _mongo_id = ( + _entity_dict["avalon_attrs"][CUST_ATTR_ID_KEY] + ) _av_ent_by_mongo_id = self.avalon_ents_by_id.get( _mongo_id ) From 0fccef46b82247d77ee46019ace506fcb8d3779c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jul 2020 12:27:25 +0200 Subject: [PATCH 19/33] tags.json moved from presets to nukestudio host --- pype/hosts/nukestudio/tags.json | 262 ++++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 pype/hosts/nukestudio/tags.json diff --git a/pype/hosts/nukestudio/tags.json b/pype/hosts/nukestudio/tags.json new file mode 100644 index 0000000000..56fcfcbce9 --- /dev/null +++ b/pype/hosts/nukestudio/tags.json @@ -0,0 +1,262 @@ +{ + "Hierarchy": { + "editable": "1", + "note": "{folder}/{sequence}/{shot}", + "icon": { + "path": "hierarchy.png" + }, + "metadata": { + "folder": "FOLDER_NAME", + "shot": "{clip}", + "track": "{track}", + "sequence": "{sequence}", + "episode": "EPISODE_NAME", + "root": "{projectroot}" + } + }, + "Source Resolution": { + "editable": "1", + "note": "Use source resolution", + "icon": { + "path": "resolution.png" + }, + "metadata": { + "family": "resolution" + } + }, + "Retiming": { + "editable": "1", + "note": "Clip has retime or TimeWarp effects (or multiple effects stacked on the clip)", + "icon": { + "path": "retiming.png" + }, + "metadata": { + "family": "retiming", + "marginIn": 1, + "marginOut": 1 + } + }, + "Frame start": { + "editable": "1", + "note": "Starting frame for comps. \n\n> Use `value` and add either number or write `source` (if you want to preserve source frame numbering)", + "icon": { + "path": "icons:TagBackground.png" + }, + "metadata": { + "family": "frameStart", + "value": "1001" + } + }, + "[Lenses]": { + "Set lense here": { + "editable": "1", + "note": "Adjust parameters of your lense and then drop to clip. Remember! You can always overwrite on clip", + "icon": { + "path": "lense.png" + }, + "metadata": { + "focalLengthMm": 57 + + } + } + }, + "[Subsets]": { + "Audio": { + "editable": "1", + "note": "Export with Audio", + "icon": { + "path": "volume.png" + }, + "metadata": { + "family": "audio", + "subset": "main" + } + }, + "plateFg": { + "editable": "1", + "note": "Add to publish to \"forground\" subset. Change metadata subset name if different order number", + "icon": { + "path": "z_layer_fg.png" + }, + "metadata": { + "family": "plate", + "subset": "Fg01" + } + }, + "plateBg": { + "editable": "1", + "note": "Add to publish to \"background\" subset. Change metadata subset name if different order number", + "icon": { + "path": "z_layer_bg.png" + }, + "metadata": { + "family": "plate", + "subset": "Bg01" + } + }, + "plateRef": { + "editable": "1", + "note": "Add to publish to \"reference\" subset.", + "icon": { + "path": "icons:Reference.png" + }, + "metadata": { + "family": "plate", + "subset": "Ref" + } + }, + "plateMain": { + "editable": "1", + "note": "Add to publish to \"main\" subset.", + "icon": { + "path": "z_layer_main.png" + }, + "metadata": { + "family": "plate", + "subset": "main" + } + }, + "plateProxy": { + "editable": "1", + "note": "Add to publish to \"proxy\" subset.", + "icon": { + "path": "z_layer_main.png" + }, + "metadata": { + "family": "plate", + "subset": "proxy" + } + }, + "review": { + "editable": "1", + "note": "Upload to Ftrack as review component.", + "icon": { + "path": "review.png" + }, + "metadata": { + "family": "review", + "track": "review" + } + } + }, + "[Handles]": { + "start: add 20 frames": { + "editable": "1", + "note": "Adding frames to start of selected clip", + "icon": { + "path": "3_add_handles_start.png" + }, + "metadata": { + "family": "handles", + "value": "20", + "args": "{'op':'add','where':'start'}" + } + }, + "start: add 10 frames": { + "editable": "1", + "note": "Adding frames to start of selected clip", + "icon": { + "path": "3_add_handles_start.png" + }, + "metadata": { + "family": "handles", + "value": "10", + "args": "{'op':'add','where':'start'}" + } + }, + "start: add 5 frames": { + "editable": "1", + "note": "Adding frames to start of selected clip", + "icon": { + "path": "3_add_handles_start.png" + }, + "metadata": { + "family": "handles", + "value": "5", + "args": "{'op':'add','where':'start'}" + } + }, + "start: add 0 frames": { + "editable": "1", + "note": "Adding frames to start of selected clip", + "icon": { + "path": "3_add_handles_start.png" + }, + "metadata": { + "family": "handles", + "value": "0", + "args": "{'op':'add','where':'start'}" + } + }, + "end: add 20 frames": { + "editable": "1", + "note": "Adding frames to end of selected clip", + "icon": { + "path": "1_add_handles_end.png" + }, + "metadata": { + "family": "handles", + "value": "20", + "args": "{'op':'add','where':'end'}" + } + }, + "end: add 10 frames": { + "editable": "1", + "note": "Adding frames to end of selected clip", + "icon": { + "path": "1_add_handles_end.png" + }, + "metadata": { + "family": "handles", + "value": "10", + "args": "{'op':'add','where':'end'}" + } + }, + "end: add 5 frames": { + "editable": "1", + "note": "Adding frames to end of selected clip", + "icon": { + "path": "1_add_handles_end.png" + }, + "metadata": { + "family": "handles", + "value": "5", + "args": "{'op':'add','where':'end'}" + } + }, + "end: add 0 frames": { + "editable": "1", + "note": "Adding frames to end of selected clip", + "icon": { + "path": "1_add_handles_end.png" + }, + "metadata": { + "family": "handles", + "value": "0", + "args": "{'op':'add','where':'end'}" + } + } + }, + "NukeScript": { + "editable": "1", + "note": "Collecting track items to Nuke scripts.", + "icon": { + "path": "icons:TagNuke.png" + }, + "metadata": { + "family": "nukescript", + "subset": "main" + } + }, + "Comment": { + "editable": "1", + "note": "Comment on a shot.", + "icon": { + "path": "icons:TagComment.png" + }, + "metadata": { + "family": "comment", + "subset": "main" + } + } +} From aa6a7bc79429d42cb8044d34553feb773142f8ca Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jul 2020 12:31:01 +0200 Subject: [PATCH 20/33] nukestudio tags.py load tags data from json next to it instead of using presets --- pype/hosts/nukestudio/tags.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pype/hosts/nukestudio/tags.py b/pype/hosts/nukestudio/tags.py index c97f13d17c..c2b1d0d728 100644 --- a/pype/hosts/nukestudio/tags.py +++ b/pype/hosts/nukestudio/tags.py @@ -1,16 +1,22 @@ import re import os +import json import hiero -from pype.api import ( - config, - Logger -) +from pype.api import Logger from avalon import io log = Logger().get_logger(__name__, "nukestudio") +def tag_data(): + current_dir = os.path.dirname(__file__) + json_path = os.path.join(current_dir, "tags.json") + with open(json_path, "r") as json_stream: + data = json.load(json_stream) + return data + + def create_tag(key, value): """ Creating Tag object. @@ -58,13 +64,9 @@ def add_tags_from_presets(): return log.debug("Setting default tags on project: {}".format(project.name())) - - # get all presets - presets = config.get_presets() - # get nukestudio tag.json from presets - nks_pres = presets["nukestudio"] - nks_pres_tags = nks_pres.get("tags", None) + # get nukestudio tags.json + nks_pres_tags = tag_data() # Get project task types. tasks = io.find_one({"type": "project"})["config"]["tasks"] From 360264117ee62d92cd58da90fcc9f61567ec0885 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 22 Jul 2020 17:07:07 +0100 Subject: [PATCH 21/33] Adding "audioMain" to Nuke reviews. - also added get_latest_version method to library. --- pype/api.py | 6 ++-- pype/lib.py | 37 +++++++++++++++++++++ pype/plugins/nuke/publish/collect_review.py | 18 ++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/pype/api.py b/pype/api.py index 44a31f2626..ce13688d13 100644 --- a/pype/api.py +++ b/pype/api.py @@ -40,7 +40,8 @@ from .lib import ( get_version_from_path, get_last_version_from_path, modified_environ, - add_tool_to_environment + add_tool_to_environment, + get_latest_version ) # Special naming case for subprocess since its a built-in method. @@ -85,5 +86,6 @@ __all__ = [ "modified_environ", "add_tool_to_environment", - "subprocess" + "subprocess", + "get_latest_version" ] diff --git a/pype/lib.py b/pype/lib.py index 87808e53f5..29e02ffd57 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1387,3 +1387,40 @@ def ffprobe_streams(path_to_file): popen_output = popen.communicate()[0] log.debug("FFprobe output: {}".format(popen_output)) return json.loads(popen_output)["streams"] + + +def get_latest_version(asset_name, subset_name): + """Retrieve latest version from `asset_name`, and `subset_name`. + + Args: + asset_name (str): Name of asset. + subset_name (str): Name of subset. + """ + # Get asset + asset_name = io.find_one( + {"type": "asset", "name": asset_name}, projection={"name": True} + ) + + subset = io.find_one( + {"type": "subset", "name": subset_name, "parent": asset_name["_id"]}, + projection={"_id": True, "name": True}, + ) + + # Check if subsets actually exists. + assert subset, "No subsets found." + + # Get version + version_projection = { + "name": True, + "parent": True, + } + + version = io.find_one( + {"type": "version", "parent": subset["_id"]}, + projection=version_projection, + sort=[("name", -1)], + ) + + assert version, "No version found, this is a bug" + + return version diff --git a/pype/plugins/nuke/publish/collect_review.py b/pype/plugins/nuke/publish/collect_review.py index c95c94541d..e7e8da19a1 100644 --- a/pype/plugins/nuke/publish/collect_review.py +++ b/pype/plugins/nuke/publish/collect_review.py @@ -1,4 +1,7 @@ import pyblish.api +import pype.api +from avalon import io, api + import nuke @@ -23,6 +26,21 @@ class CollectReview(pyblish.api.InstancePlugin): if not node["review"].value(): return + # Add audio to instance if it exists. + try: + version = pype.api.get_latest_version( + instance.context.data["assetEntity"]["name"], "audioMain" + ) + representation = io.find_one( + {"type": "representation", "parent": version["_id"]} + ) + instance.data["audio"] = [{ + "offset": 0, + "filename": api.get_representation_path(representation) + }] + except AssertionError: + pass + instance.data["families"].append("review") instance.data['families'].append('ftrack') From c131ffefbdecf92c446ab1e00ced6051231ccfcb Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 Jul 2020 11:12:06 +0100 Subject: [PATCH 22/33] Calculate size - option to only calculate without deletion --- .../actions/action_delete_old_versions.py | 105 ++++++++++++++++-- 1 file changed, 93 insertions(+), 12 deletions(-) diff --git a/pype/modules/ftrack/actions/action_delete_old_versions.py b/pype/modules/ftrack/actions/action_delete_old_versions.py index 46652b136a..6a4c5a0cae 100644 --- a/pype/modules/ftrack/actions/action_delete_old_versions.py +++ b/pype/modules/ftrack/actions/action_delete_old_versions.py @@ -105,11 +105,34 @@ class DeleteOldVersions(BaseAction): "value": False }) + items.append(self.splitter_item) + + items.append({ + "type": "label", + "value": ( + "This will NOT delete any files and only return the " + "total size of the files." + ) + }) + items.append({ + "type": "boolean", + "name": "only_calculate", + "label": "Only calculate size of files.", + "value": False + }) + return { "items": items, "title": self.inteface_title } + def sizeof_fmt(self, num, suffix='B'): + for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: + if abs(num) < 1024.0: + return "%3.1f%s%s" % (num, unit, suffix) + num /= 1024.0 + return "%.1f%s%s" % (num, 'Yi', suffix) + def launch(self, session, entities, event): values = event["data"].get("values") if not values: @@ -117,6 +140,7 @@ class DeleteOldVersions(BaseAction): versions_count = int(values["last_versions_count"]) force_to_remove = values["force_delete_publish_folder"] + only_calculate = values["only_calculate"] _val1 = "OFF" if force_to_remove: @@ -318,10 +342,29 @@ class DeleteOldVersions(BaseAction): "Folder does not exist. Deleting it's files skipped: {}" ).format(paths_msg)) + # Size of files. + size = 0 + + if only_calculate: + if force_to_remove: + size = self.delete_whole_dir_paths( + dir_paths.values(), delete=False + ) + else: + size = self.delete_only_repre_files( + dir_paths, file_paths_by_dir, delete=False + ) + + msg = "Total size of files: " + self.sizeof_fmt(size) + + self.log.warning(msg) + + return {"success": True, "message": msg} + if force_to_remove: - self.delete_whole_dir_paths(dir_paths.values()) + size = self.delete_whole_dir_paths(dir_paths.values()) else: - self.delete_only_repre_files(dir_paths, file_paths_by_dir) + size = self.delete_only_repre_files(dir_paths, file_paths_by_dir) mongo_changes_bulk = [] for version in versions: @@ -383,17 +426,31 @@ class DeleteOldVersions(BaseAction): "message": msg } - return True + msg = "Total size of files deleted: " + self.sizeof_fmt(size) + + self.log.warning(msg) + + return {"success": True, "message": msg} + + def delete_whole_dir_paths(self, dir_paths, delete=True): + size = 0 - def delete_whole_dir_paths(self, dir_paths): for dir_path in dir_paths: # Delete all files and fodlers in dir path for root, dirs, files in os.walk(dir_path, topdown=False): for name in files: - os.remove(os.path.join(root, name)) + file_path = os.path.join(root, name) + size += os.path.getsize(file_path) + if delete: + os.remove(file_path) + self.log.debug("Removed file: {}".format(file_path)) for name in dirs: - os.rmdir(os.path.join(root, name)) + if delete: + os.rmdir(os.path.join(root, name)) + + if not delete: + continue # Delete even the folder and it's parents folders if they are empty while True: @@ -406,7 +463,11 @@ class DeleteOldVersions(BaseAction): os.rmdir(os.path.join(dir_path)) - def delete_only_repre_files(self, dir_paths, file_paths): + return size + + def delete_only_repre_files(self, dir_paths, file_paths, delete=True): + size = 0 + for dir_id, dir_path in dir_paths.items(): dir_files = os.listdir(dir_path) collections, remainders = clique.assemble(dir_files) @@ -420,8 +481,13 @@ class DeleteOldVersions(BaseAction): "File was not found: {}".format(file_path) ) continue - os.remove(file_path) - self.log.debug("Removed file: {}".format(file_path)) + + size += os.path.getsize(file_path) + + if delete: + os.remove(file_path) + self.log.debug("Removed file: {}".format(file_path)) + remainders.remove(file_path_base) continue @@ -440,21 +506,34 @@ class DeleteOldVersions(BaseAction): final_col.head = os.path.join(dir_path, final_col.head) for _file_path in final_col: if os.path.exists(_file_path): - os.remove(_file_path) + + size += os.path.getsize(_file_path) + + if delete: + os.remove(_file_path) + self.log.debug( + "Removed file: {}".format(_file_path) + ) + _seq_path = final_col.format("{head}{padding}{tail}") self.log.debug("Removed files: {}".format(_seq_path)) collections.remove(final_col) elif os.path.exists(file_path): - os.remove(file_path) - self.log.debug("Removed file: {}".format(file_path)) + size += os.path.getsize(file_path) + if delete: + os.remove(file_path) + self.log.debug("Removed file: {}".format(file_path)) else: self.log.warning( "File was not found: {}".format(file_path) ) # Delete as much as possible parent folders + if not delete: + return size + for dir_path in dir_paths.values(): while True: if not os.path.exists(dir_path): @@ -467,6 +546,8 @@ class DeleteOldVersions(BaseAction): self.log.debug("Removed folder: {}".format(dir_path)) os.rmdir(dir_path) + return size + def path_from_represenation(self, representation, anatomy): try: template = representation["data"]["template"] From 7baa3bdd6a12732dcc62bc957c815c23c96b511a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 23 Jul 2020 14:55:07 +0200 Subject: [PATCH 23/33] support for single frame renders from maya --- .../global/publish/submit_publish_job.py | 38 +++++++++++++------ .../maya/publish/submit_maya_deadline.py | 35 ++++++++++++----- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 4f32e37c17..cec5f61011 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -380,15 +380,22 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # go through aovs in expected files for aov, files in exp_files[0].items(): cols, rem = clique.assemble(files) - # we shouldn't have any reminders - if rem: - self.log.warning( - "skipping unexpected files found " - "in sequence: {}".format(rem)) - - # but we really expect only one collection, nothing else make sense - assert len(cols) == 1, "only one image sequence type is expected" + # we shouldn't have any reminders. And if we do, it should + # be just one item for single frame renders. + if not cols and rem: + assert len(rem) == 1, ("Found multiple non related files " + "to render, don't know what to do " + "with them.") + col = rem[0] + _, ext = os.path.splitext(col) + else: + # but we really expect only one collection. + # Nothing else make sense. + assert len(cols) == 1, "only one image sequence type is expected" # noqa: E501 + _, ext = os.path.splitext(cols[0].tail) + col = list(cols[0]) + self.log.debug(col) # create subset name `familyTaskSubset_AOV` group_name = 'render{}{}{}{}'.format( task[0].upper(), task[1:], @@ -396,7 +403,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): subset_name = '{}_{}'.format(group_name, aov) - staging = os.path.dirname(list(cols[0])[0]) + if isinstance(col, (list, tuple)): + staging = os.path.dirname(col[0]) + else: + staging = os.path.dirname(col) + success, rootless_staging_dir = ( self.anatomy.find_root_template_from_path(staging) ) @@ -421,13 +432,16 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): new_instance["subset"] = subset_name new_instance["subsetGroup"] = group_name - ext = cols[0].tail.lstrip(".") - # create represenation + if isinstance(col, (list, tuple)): + files = [os.path.basename(f) for f in col] + else: + files = os.path.basename(col) + rep = { "name": ext, "ext": ext, - "files": [os.path.basename(f) for f in list(cols[0])], + "files": files, "frameStart": int(instance_data.get("frameStartHandle")), "frameEnd": int(instance_data.get("frameEndHandle")), # If expectedFile are absolute, we need only filenames diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index d81d43749c..d5500f7aa8 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -20,6 +20,7 @@ import os import json import getpass import copy +import re import clique import requests @@ -108,8 +109,8 @@ def get_renderer_variables(renderlayer, root): # does not work for vray. scene = cmds.file(query=True, sceneName=True) scene, _ = os.path.splitext(os.path.basename(scene)) - filename_0 = filename_prefix.replace('', scene) - filename_0 = filename_0.replace('', renderlayer) + filename_0 = re.sub('', scene, filename_prefix, flags=re.IGNORECASE) # noqa: E501 + filename_0 = re.sub('', renderlayer, filename_0, flags=re.IGNORECASE) # noqa: E501 filename_0 = "{}.{}.{}".format( filename_0, "#" * int(padding), extension) filename_0 = os.path.normpath(os.path.join(root, filename_0)) @@ -375,16 +376,32 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): if isinstance(exp[0], dict): # we have aovs and we need to iterate over them for _aov, files in exp[0].items(): - col = clique.assemble(files)[0][0] - output_file = col.format('{head}{padding}{tail}') - payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file # noqa: E501 + col, rem = clique.assemble(files) + if not col and rem: + # we couldn't find any collections but have + # individual files. + assert len(rem) == 1, ("Found multiple non related files " + "to render, don't know what to do " + "with them.") + payload['JobInfo']['OutputFilename' + str(exp_index)] = rem[0] # noqa: E501 + output_file = rem[0] + else: + output_file = col.format('{head}{padding}{tail}') + payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file # noqa: E501 output_filenames[exp_index] = output_file exp_index += 1 else: - col = clique.assemble(files)[0][0] - output_file = col.format('{head}{padding}{tail}') - payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file - # OutputFilenames[exp_index] = output_file + col, rem = clique.assemble(files) + if not col and rem: + # we couldn't find any collections but have + # individual files. + assert len(rem) == 1, ("Found multiple non related files " + "to render, don't know what to do " + "with them.") + payload['JobInfo']['OutputFilename' + str(exp_index)] = rem[0] # noqa: E501 + else: + output_file = col.format('{head}{padding}{tail}') + payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file # noqa: E501 plugin = payload["JobInfo"]["Plugin"] self.log.info("using render plugin : {}".format(plugin)) From bd7876fe20acd526c56cdca408dd89805e7ce588 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 24 Jul 2020 19:52:20 +0200 Subject: [PATCH 24/33] bake custom attributes to camera during export --- .../maya/publish/extract_camera_alembic.py | 9 ++++ .../maya/publish/extract_camera_mayaAscii.py | 50 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/pype/plugins/maya/publish/extract_camera_alembic.py b/pype/plugins/maya/publish/extract_camera_alembic.py index cc090760ff..c61ec5e19e 100644 --- a/pype/plugins/maya/publish/extract_camera_alembic.py +++ b/pype/plugins/maya/publish/extract_camera_alembic.py @@ -19,6 +19,7 @@ class ExtractCameraAlembic(pype.api.Extractor): label = "Camera (Alembic)" hosts = ["maya"] families = ["camera"] + bake_attributes = [] def process(self, instance): @@ -66,6 +67,14 @@ class ExtractCameraAlembic(pype.api.Extractor): job_str += ' -file "{0}"'.format(path) + # bake specified attributes in preset + assert isinstance(self.bake_attributes, (list, tuple)), ( + "Attributes to bake must be specified as a list" + ) + for attr in self.bake_attributes: + self.log.info("Adding {} attribute".format(attr)) + job_str += " -attr {0}".format(attr) + with lib.evaluation("off"): with avalon.maya.suspended_refresh(): cmds.AbcExport(j=job_str, verbose=False) diff --git a/pype/plugins/maya/publish/extract_camera_mayaAscii.py b/pype/plugins/maya/publish/extract_camera_mayaAscii.py index 973d8d452a..eb3b1671de 100644 --- a/pype/plugins/maya/publish/extract_camera_mayaAscii.py +++ b/pype/plugins/maya/publish/extract_camera_mayaAscii.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Extract camera to Maya file.""" import os from maya import cmds @@ -65,6 +67,45 @@ def unlock(plug): cmds.disconnectAttr(source, destination) +def bake_attribute(camera, + attributes, + step=1.0, simulation=True, frame_range=None): + """Bake specified attributes on camera. + + Args: + camera (str): Camera name. + attributes (list): List of attributes to bake. + step (float): Animation step used for baking. + simulation (bool): Perform simulation instead of just evaluating + each attribute separately over the range of time. + frame_rage (list, tuple): start and end frame to define range. + + .. See also: + http://download.autodesk.com/us/maya/2011help/Commandspython/bakeResults.html + + """ + + if frame_range is None: + frame_range = [cmds.playbackOptions(query=True, minTime=True), + cmds.playbackOptions(query=True, maxTime=True)] + + # If frame range is single frame bake one frame more, + # otherwise maya.cmds.bakeResults gets confused + if frame_range[1] == frame_range[0]: + frame_range[1] += 1 + + assert isinstance(attributes, (list, tuple)), ( + "Attributes to bake must be specified as a list" + ) + + with lib.keytangent_default(in_tangent_type='auto', + out_tangent_type='auto'): + cmds.bakeResults(camera, attribute=attributes, + simulation=simulation, + time=(frame_range[0], frame_range[1]), + sampleBy=step) + + class ExtractCameraMayaAscii(pype.api.Extractor): """Extract a Camera as Maya Ascii. @@ -84,6 +125,7 @@ class ExtractCameraMayaAscii(pype.api.Extractor): label = "Camera (Maya Ascii)" hosts = ["maya"] families = ["camera"] + bake_attributes = [] def process(self, instance): @@ -148,6 +190,14 @@ class ExtractCameraMayaAscii(pype.api.Extractor): unlock(plug) cmds.setAttr(plug, value) + if self.bake_attributes: + self.log.info( + "Baking attributes: {}".format( + self.bake_attributes)) + bake_attribute( + cam, self.bake_attributes, + frame_range=range_with_handles, step=step) + self.log.info("Performing extraction..") cmds.select(baked_shapes, noExpand=True) cmds.file(path, From a47ffde54203adcff6b87cf2d379ab402fea4fb1 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 27 Jul 2020 11:05:32 +0200 Subject: [PATCH 25/33] sanitize camera names --- pype/hosts/maya/expected_files.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/pype/hosts/maya/expected_files.py b/pype/hosts/maya/expected_files.py index a7204cba93..77d55eb1c1 100644 --- a/pype/hosts/maya/expected_files.py +++ b/pype/hosts/maya/expected_files.py @@ -158,6 +158,25 @@ class AExpectedFiles: """To be implemented by renderer class.""" pass + def sanitize_camera_name(self, camera): + """Sanitize camera name. + + Remove Maya illegal characters from camera name. + + Args: + camera (str): Maya camera name. + + Returns: + (str): sanitized camera name + + Example: + >>> sanizite_camera_name('test:camera_01') + test_camera_01 + + """ + sanitized = re.sub('[^0-9a-zA-Z_]+', '_', camera) + return sanitized + def get_renderer_prefix(self): """Return prefix for specific renderer. @@ -252,7 +271,7 @@ class AExpectedFiles: mappings = ( (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), - (R_SUBSTITUTE_CAMERA_TOKEN, cam), + (R_SUBSTITUTE_CAMERA_TOKEN, self.sanitize_camera_name(cam)), # this is required to remove unfilled aov token, for example # in Redshift (R_REMOVE_AOV_TOKEN, ""), @@ -287,7 +306,8 @@ class AExpectedFiles: mappings = ( (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), - (R_SUBSTITUTE_CAMERA_TOKEN, cam), + (R_SUBSTITUTE_CAMERA_TOKEN, + self.sanitize_camera_name(cam)), (R_SUBSTITUTE_AOV_TOKEN, aov[0]), (R_CLEAN_FRAME_TOKEN, ""), (R_CLEAN_EXT_TOKEN, ""), @@ -314,7 +334,8 @@ class AExpectedFiles: # camera name to AOV to allow per camera AOVs. aov_name = aov[0] if len(layer_data["cameras"]) > 1: - aov_name = "{}_{}".format(aov[0], cam) + aov_name = "{}_{}".format(aov[0], + self.sanitize_camera_name(cam)) aov_file_list[aov_name] = aov_files file_prefix = layer_data["filePrefix"] From 86119ea823c7436461e6aa4ca3681ea3febcd9d3 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 27 Jul 2020 13:37:16 +0200 Subject: [PATCH 26/33] support for updating renderSetup settings --- pype/plugins/maya/load/load_rendersetup.py | 63 +++++++++++++++++++--- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/pype/plugins/maya/load/load_rendersetup.py b/pype/plugins/maya/load/load_rendersetup.py index b38e2988b1..fae79548a5 100644 --- a/pype/plugins/maya/load/load_rendersetup.py +++ b/pype/plugins/maya/load/load_rendersetup.py @@ -1,14 +1,23 @@ -from avalon import api -import maya.app.renderSetup.model.renderSetup as renderSetup -from avalon.maya import lib -from maya import cmds +# -*- coding: utf-8 -*- +"""Load and update RenderSetup settings. + +Working with RenderSetup setting is Maya is done utilizing json files. +When this json is loaded, it will overwrite all settings on RenderSetup +instance. +""" + import json +from avalon import api +from avalon.maya import lib +from pype.hosts.maya import lib as pypelib + +from maya import cmds +import maya.app.renderSetup.model.renderSetup as renderSetup + class RenderSetupLoader(api.Loader): - """ - This will load json preset for RenderSetup, overwriting current one. - """ + """Load json preset for RenderSetup overwriting current one.""" families = ["rendersetup"] representations = ["json"] @@ -19,7 +28,7 @@ class RenderSetupLoader(api.Loader): color = "orange" def load(self, context, name, namespace, data): - + """Load RenderSetup settings.""" from avalon.maya.pipeline import containerise # from pype.hosts.maya.lib import namespaced @@ -48,3 +57,41 @@ class RenderSetupLoader(api.Loader): nodes=nodes, context=context, loader=self.__class__.__name__) + + def remove(self, container): + """Remove RenderSetup settings instance.""" + from maya import cmds + + namespace = container["namespace"] + container_name = container["objectName"] + + self.log.info("Removing '%s' from Maya.." % container["name"]) + + container_content = cmds.sets(container_name, query=True) + nodes = cmds.ls(container_content, long=True) + + nodes.append(container_name) + + try: + cmds.delete(nodes) + except ValueError: + # Already implicitly deleted by Maya upon removing reference + pass + + cmds.namespace(removeNamespace=namespace, deleteNamespaceContent=True) + + def update(self, container, representation): + """Update RenderSetup setting by overwriting existing settings.""" + pypelib.show_message( + "Render setup update", + "Render setup setting will be overwritten by new version. All " + "setting specified by user not included in loaded version " + "will be lost.") + path = api.get_representation_path(representation) + with open(path, "r") as file: + renderSetup.instance().decode( + json.load(file), renderSetup.DECODE_AND_OVERWRITE, None) + + def switch(self, container, representation): + """Switch representations.""" + self.update(container, representation) From 7ce31ba1ecf45a5a03b5c4d3e3d19c1b780c9e4d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 28 Jul 2020 13:14:20 +0200 Subject: [PATCH 27/33] small code fixes --- pype/plugins/maya/load/load_rendersetup.py | 23 ++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/pype/plugins/maya/load/load_rendersetup.py b/pype/plugins/maya/load/load_rendersetup.py index fae79548a5..45a314a9d1 100644 --- a/pype/plugins/maya/load/load_rendersetup.py +++ b/pype/plugins/maya/load/load_rendersetup.py @@ -7,6 +7,8 @@ instance. """ import json +import six +import sys from avalon import api from avalon.maya import lib @@ -38,7 +40,7 @@ class RenderSetupLoader(api.Loader): prefix="_" if asset[0].isdigit() else "", suffix="_", ) - + self.log.info(">>> loading json [ {} ]".format(self.fname)) with open(self.fname, "r") as file: renderSetup.instance().decode( json.load(file), renderSetup.DECODE_AND_OVERWRITE, None) @@ -51,6 +53,7 @@ class RenderSetupLoader(api.Loader): if not nodes: return + self.log.info(">>> containerising [ {} ]".format(name)) return containerise( name=name, namespace=namespace, @@ -62,7 +65,6 @@ class RenderSetupLoader(api.Loader): """Remove RenderSetup settings instance.""" from maya import cmds - namespace = container["namespace"] container_name = container["objectName"] self.log.info("Removing '%s' from Maya.." % container["name"]) @@ -78,8 +80,6 @@ class RenderSetupLoader(api.Loader): # Already implicitly deleted by Maya upon removing reference pass - cmds.namespace(removeNamespace=namespace, deleteNamespaceContent=True) - def update(self, container, representation): """Update RenderSetup setting by overwriting existing settings.""" pypelib.show_message( @@ -89,8 +89,19 @@ class RenderSetupLoader(api.Loader): "will be lost.") path = api.get_representation_path(representation) with open(path, "r") as file: - renderSetup.instance().decode( - json.load(file), renderSetup.DECODE_AND_OVERWRITE, None) + try: + renderSetup.instance().decode( + json.load(file), renderSetup.DECODE_AND_OVERWRITE, None) + except Exception: + self.log.error("There were errors during loading") + six.reraise(*sys.exc_info()) + + # Update metadata + node = container["objectName"] + cmds.setAttr("{}.representation".format(node), + str(representation["_id"]), + type="string") + self.log.info("... updated") def switch(self, container, representation): """Switch representations.""" From 832fd9fa66b67c5a6dacb7817745f84cfbea4e4b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 29 Jul 2020 16:17:15 +0100 Subject: [PATCH 28/33] Fix multiple attributes on the same node overwriting. --- pype/plugins/maya/publish/validate_attributes.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pype/plugins/maya/publish/validate_attributes.py b/pype/plugins/maya/publish/validate_attributes.py index 6ecebfa107..a77fbe5e93 100644 --- a/pype/plugins/maya/publish/validate_attributes.py +++ b/pype/plugins/maya/publish/validate_attributes.py @@ -62,9 +62,16 @@ class ValidateAttributes(pyblish.api.ContextPlugin): for family in families: for preset in presets[family]: [node_name, attribute_name] = preset.split(".") - attributes.update( - {node_name: {attribute_name: presets[family][preset]}} - ) + try: + attributes[node_name].update( + {attribute_name: presets[family][preset]} + ) + except KeyError: + attributes.update({ + node_name: { + attribute_name: presets[family][preset] + } + }) # Get invalid attributes. nodes = pm.ls() From 3925ddc5aeabe392230224247bd909c73031fed2 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 29 Jul 2020 17:34:54 +0100 Subject: [PATCH 29/33] Instance in Same Context for Nuke Moved the equivalent Maya plugin into global and merged with Nuke. Fixed Nuke instances collection not using node data for asset. --- .../publish/validate_instance_in_context.py | 133 ++++++++++++++++++ .../publish/validate_instance_in_context.py | 108 -------------- .../plugins/nuke/publish/collect_instances.py | 2 +- 3 files changed, 134 insertions(+), 109 deletions(-) create mode 100644 pype/plugins/global/publish/validate_instance_in_context.py delete mode 100644 pype/plugins/maya/publish/validate_instance_in_context.py diff --git a/pype/plugins/global/publish/validate_instance_in_context.py b/pype/plugins/global/publish/validate_instance_in_context.py new file mode 100644 index 0000000000..a4fc555161 --- /dev/null +++ b/pype/plugins/global/publish/validate_instance_in_context.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +"""Validate if instance asset is the same as context asset.""" +from __future__ import absolute_import + +import pyblish.api +import pype.api + + +class SelectInvalidInstances(pyblish.api.Action): + """Select invalid instances in Outliner.""" + + label = "Select Instances" + icon = "briefcase" + on = "failed" + + def process(self, context, plugin): + """Process invalid validators and select invalid instances.""" + # Get the errored instances + failed = [] + for result in context.data["results"]: + if result["error"] is None: + continue + if result["instance"] is None: + continue + if result["instance"] in failed: + continue + if result["plugin"] != plugin: + continue + + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + + if instances: + self.log.info( + "Selecting invalid nodes: %s" % ", ".join( + [str(x) for x in instances] + ) + ) + self.select(instances) + else: + self.log.info("No invalid nodes found.") + self.deselect() + + def select(self, instances): + if "nuke" in pyblish.api.registered_hosts(): + import avalon.nuke.lib + import nuke + avalon.nuke.lib.select_nodes( + [nuke.toNode(str(x)) for x in instances] + ) + + if "maya" in pyblish.api.registered_hosts(): + from maya import cmds + cmds.select(instances, replace=True, noExpand=True) + + def deselect(self): + if "nuke" in pyblish.api.registered_hosts(): + import avalon.nuke.lib + avalon.nuke.lib.reset_selection() + + if "maya" in pyblish.api.registered_hosts(): + from maya import cmds + cmds.select(deselect=True) + + +class RepairSelectInvalidInstances(pyblish.api.Action): + """Repair the instance asset.""" + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + # Get the errored instances + failed = [] + for result in context.data["results"]: + if result["error"] is None: + continue + if result["instance"] is None: + continue + if result["instance"] in failed: + continue + if result["plugin"] != plugin: + continue + + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + + context_asset = context.data["assetEntity"]["name"] + for instance in instances: + self.set_attribute(instance, context_asset) + + def set_attribute(self, instance, context_asset): + if "nuke" in pyblish.api.registered_hosts(): + import nuke + nuke.toNode( + instance.data.get("name") + )["avalon:asset"].setValue(context_asset) + + if "maya" in pyblish.api.registered_hosts(): + from maya import cmds + cmds.setAttr( + instance.data.get("name") + ".asset", + context_asset, + type="string" + ) + + +class ValidateInstanceInContext(pyblish.api.InstancePlugin): + """Validator to check if instance asset match context asset. + + When working in per-shot style you always publish data in context of + current asset (shot). This validator checks if this is so. It is optional + so it can be disabled when needed. + + Action on this validator will select invalid instances in Outliner. + """ + + order = pype.api.ValidateContentsOrder + label = "Instance in same Context" + optional = True + hosts = ["maya", "nuke"] + actions = [SelectInvalidInstances, RepairSelectInvalidInstances] + + def process(self, instance): + asset = instance.data.get("asset") + context_asset = instance.context.data["assetEntity"]["name"] + msg = "{} has asset {}".format(instance.name, asset) + assert asset == context_asset, msg diff --git a/pype/plugins/maya/publish/validate_instance_in_context.py b/pype/plugins/maya/publish/validate_instance_in_context.py deleted file mode 100644 index 542249bb2d..0000000000 --- a/pype/plugins/maya/publish/validate_instance_in_context.py +++ /dev/null @@ -1,108 +0,0 @@ -# -*- coding: utf-8 -*- -"""Validate if instance asset is the same as context asset.""" -from __future__ import absolute_import -import pyblish.api -from pype.action import get_errored_instances_from_context -import pype.api - - -class SelectInvalidInstances(pyblish.api.Action): - """Select invalid instances in Outliner.""" - - label = "Show Instances" - icon = "briefcase" - on = "failed" - - def process(self, context, plugin): - """Process invalid validators and select invalid instances.""" - try: - from maya import cmds - except ImportError: - raise ImportError("Current host is not Maya") - - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) - - # Get the invalid nodes for the plug-ins - self.log.info("Finding invalid nodes..") - invalid = list() - for _instance in instances: - invalid_instances = plugin.get_invalid(context) - if invalid_instances: - if isinstance(invalid_instances, (list, tuple)): - invalid.extend(invalid_instances) - else: - self.log.warning("Plug-in returned to be invalid, " - "but has no selectable nodes.") - - # Ensure unique (process each node only once) - invalid = list(set(invalid)) - - if invalid: - self.log.info("Selecting invalid nodes: %s" % ", ".join(invalid)) - cmds.select(invalid, replace=True, noExpand=True) - else: - self.log.info("No invalid nodes found.") - cmds.select(deselect=True) - - -class RepairSelectInvalidInstances(pyblish.api.Action): - """Repair the instance asset.""" - - label = "Repair" - icon = "wrench" - on = "failed" - - def process(self, context, plugin): - from maya import cmds - # Get the errored instances - failed = [] - for result in context.data["results"]: - if (result["error"] is not None and result["instance"] is not None - and result["instance"] not in failed): - failed.append(result["instance"]) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(failed, plugin) - context_asset = context.data["assetEntity"]["name"] - for instance in instances: - cmds.setAttr(instance.data.get("name") + ".asset", - context_asset, type="string") - - -class ValidateInstanceInContext(pyblish.api.ContextPlugin): - """Validator to check if instance asset match context asset. - - When working in per-shot style you always publish data in context of - current asset (shot). This validator checks if this is so. It is optional - so it can be disabled when needed. - - Action on this validator will select invalid instances in Outliner. - """ - - order = pype.api.ValidateContentsOrder - label = "Instance in same Context" - optional = True - actions = [SelectInvalidInstances, RepairSelectInvalidInstances] - - @classmethod - def get_invalid(cls, context): - """Get invalid instances.""" - invalid = [] - context_asset = context.data["assetEntity"]["name"] - cls.log.info("we are in {}".format(context_asset)) - for instance in context: - asset = instance.data.get("asset") - if asset != context_asset: - cls.log.warning("{} has asset {}".format(instance.name, asset)) - invalid.append(instance.name) - - return invalid - - def process(self, context): - """Check instances.""" - invalid = self.get_invalid(context) - if invalid: - raise AssertionError("Some instances doesn't share same context") diff --git a/pype/plugins/nuke/publish/collect_instances.py b/pype/plugins/nuke/publish/collect_instances.py index 0bbede11c0..9085e12bd8 100644 --- a/pype/plugins/nuke/publish/collect_instances.py +++ b/pype/plugins/nuke/publish/collect_instances.py @@ -106,7 +106,7 @@ class CollectNukeInstances(pyblish.api.ContextPlugin): instance.data.update({ "subset": subset, - "asset": os.environ["AVALON_ASSET"], + "asset": avalon_knob_data["asset"], "label": node.name(), "name": node.name(), "subset": subset, From 907ebe17604142a2487879a9004247bc81cf2c54 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 30 Jul 2020 08:46:53 +0100 Subject: [PATCH 30/33] Fix viewer input process node return as Viewer node --- pype/hosts/nuke/lib.py | 2 +- pype/plugins/nuke/publish/extract_thumbnail.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/hosts/nuke/lib.py b/pype/hosts/nuke/lib.py index 72a8836a03..8c0e37b15d 100644 --- a/pype/hosts/nuke/lib.py +++ b/pype/hosts/nuke/lib.py @@ -1445,7 +1445,7 @@ class ExporterReview: anlib.reset_selection() ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" in n.Class()]: + if "Viewer" == n.Class()]: ip = v['input_process'].getValue() ipn = v['input_process_node'].getValue() if "VIEWER_INPUT" not in ipn and ip: diff --git a/pype/plugins/nuke/publish/extract_thumbnail.py b/pype/plugins/nuke/publish/extract_thumbnail.py index 5e9302a01a..a3ef09bc9f 100644 --- a/pype/plugins/nuke/publish/extract_thumbnail.py +++ b/pype/plugins/nuke/publish/extract_thumbnail.py @@ -152,7 +152,7 @@ class ExtractThumbnail(pype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" in n.Class()]: + if "Viewer" == n.Class()]: ip = v['input_process'].getValue() ipn = v['input_process_node'].getValue() if "VIEWER_INPUT" not in ipn and ip: From c59eeab6e60aae9f01bcd399d387259e41646a2d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 31 Jul 2020 15:35:06 +0200 Subject: [PATCH 31/33] some code cleanup --- pype/plugins/global/publish/cleanup.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/pype/plugins/global/publish/cleanup.py b/pype/plugins/global/publish/cleanup.py index 3ab41f90ca..bca540078f 100644 --- a/pype/plugins/global/publish/cleanup.py +++ b/pype/plugins/global/publish/cleanup.py @@ -1,11 +1,18 @@ +# -*- coding: utf-8 -*- +"""Cleanup leftover files from publish.""" import os import shutil import pyblish.api def clean_renders(instance): - transfers = instance.data.get("transfers", list()) + """Delete renders after publishing. + Args: + instance (pyblish.api.Instace): Instance to work on. + + """ + transfers = instance.data.get("transfers", list()) current_families = instance.data.get("families", list()) instance_family = instance.data.get("family", None) dirnames = [] @@ -40,6 +47,7 @@ class CleanUp(pyblish.api.InstancePlugin): active = True def process(self, instance): + """Plugin entry point.""" # Get the errored instances failed = [] for result in instance.context.data["results"]: @@ -52,7 +60,7 @@ class CleanUp(pyblish.api.InstancePlugin): ) ) - self.log.info("Cleaning renders ...") + self.log.info("Performing cleanup on {}".format(instance)) clean_renders(instance) if [ef for ef in self.exclude_families @@ -60,16 +68,17 @@ class CleanUp(pyblish.api.InstancePlugin): return import tempfile - staging_dir = instance.data.get("stagingDir", None) - if not staging_dir or not os.path.exists(staging_dir): - self.log.info("No staging directory found: %s" % staging_dir) - return - temp_root = tempfile.gettempdir() + staging_dir = instance.data.get("stagingDir", None) + if not os.path.normpath(staging_dir).startswith(temp_root): self.log.info("Skipping cleanup. Staging directory is not in the " "temp folder: %s" % staging_dir) return - self.log.info("Removing staging directory ...") + if not staging_dir or not os.path.exists(staging_dir): + self.log.info("No staging directory found: %s" % staging_dir) + return + + self.log.info("Removing staging directory {}".format(staging_dir)) shutil.rmtree(staging_dir) From 5b21943ed840e635634339158e532e69a4b3b7d3 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 31 Jul 2020 17:43:57 +0200 Subject: [PATCH 32/33] make decision about ma/mb configurable per family --- ...yaAscii.py => extract_camera_mayaScene.py} | 39 ++++++++++++----- ...ascii_raw.py => extract_maya_scene_raw.py} | 35 +++++++++++----- pype/plugins/maya/publish/extract_model.py | 32 ++++++++++---- pype/plugins/maya/publish/extract_rig.py | 33 +++++++++++---- pype/plugins/maya/publish/extract_yeti_rig.py | 42 +++++++++++++------ 5 files changed, 131 insertions(+), 50 deletions(-) rename pype/plugins/maya/publish/{extract_camera_mayaAscii.py => extract_camera_mayaScene.py} (82%) rename pype/plugins/maya/publish/{extract_maya_ascii_raw.py => extract_maya_scene_raw.py} (60%) diff --git a/pype/plugins/maya/publish/extract_camera_mayaAscii.py b/pype/plugins/maya/publish/extract_camera_mayaScene.py similarity index 82% rename from pype/plugins/maya/publish/extract_camera_mayaAscii.py rename to pype/plugins/maya/publish/extract_camera_mayaScene.py index 973d8d452a..03dde031e9 100644 --- a/pype/plugins/maya/publish/extract_camera_mayaAscii.py +++ b/pype/plugins/maya/publish/extract_camera_mayaScene.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Extract camera as Maya Scene.""" import os from maya import cmds @@ -65,8 +67,8 @@ def unlock(plug): cmds.disconnectAttr(source, destination) -class ExtractCameraMayaAscii(pype.api.Extractor): - """Extract a Camera as Maya Ascii. +class ExtractCameraMayaScene(pype.api.Extractor): + """Extract a Camera as Maya Scene. This will create a duplicate of the camera that will be baked *with* substeps and handles for the required frames. This temporary duplicate @@ -81,13 +83,28 @@ class ExtractCameraMayaAscii(pype.api.Extractor): """ - label = "Camera (Maya Ascii)" + label = "Camera (Maya Scene)" hosts = ["maya"] families = ["camera"] + scene_type = "ma" def process(self, instance): - + """Plugin entry point.""" # get settings + ext_mapping = instance.context.data["presets"]["maya"].get("ext_mapping") # noqa: E501 + if ext_mapping: + self.log.info("Looking in presets for scene type ...") + # use extension mapping for first family found + for family in self.families: + try: + self.scene_type = ext_mapping[family] + self.log.info( + "Using {} as scene type".format(self.scene_type)) + break + except AttributeError: + # no preset found + pass + framerange = [instance.data.get("frameStart", 1), instance.data.get("frameEnd", 1)] handles = instance.data.get("handles", 0) @@ -95,7 +112,7 @@ class ExtractCameraMayaAscii(pype.api.Extractor): bake_to_worldspace = instance.data("bakeToWorldSpace", True) if not bake_to_worldspace: - self.log.warning("Camera (Maya Ascii) export only supports world" + self.log.warning("Camera (Maya Scene) export only supports world" "space baked camera extractions. The disabled " "bake to world space is ignored...") @@ -115,7 +132,7 @@ class ExtractCameraMayaAscii(pype.api.Extractor): # Define extract output file path dir_path = self.staging_dir(instance) - filename = "{0}.ma".format(instance.name) + filename = "{0}.{1}".format(instance.name, self.scene_type) path = os.path.join(dir_path, filename) # Perform extraction @@ -152,7 +169,7 @@ class ExtractCameraMayaAscii(pype.api.Extractor): cmds.select(baked_shapes, noExpand=True) cmds.file(path, force=True, - typ="mayaAscii", + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 exportSelected=True, preserveReferences=False, constructionHistory=False, @@ -164,15 +181,15 @@ class ExtractCameraMayaAscii(pype.api.Extractor): # Delete the baked hierarchy if bake_to_worldspace: cmds.delete(baked) - - massage_ma_file(path) + if self.scene_type == "ma": + massage_ma_file(path) if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'ma', - 'ext': 'ma', + 'name': self.scene_type, + 'ext': self.scene_type, 'files': filename, "stagingDir": dir_path, } diff --git a/pype/plugins/maya/publish/extract_maya_ascii_raw.py b/pype/plugins/maya/publish/extract_maya_scene_raw.py similarity index 60% rename from pype/plugins/maya/publish/extract_maya_ascii_raw.py rename to pype/plugins/maya/publish/extract_maya_scene_raw.py index 895b6acbfe..2971572552 100644 --- a/pype/plugins/maya/publish/extract_maya_ascii_raw.py +++ b/pype/plugins/maya/publish/extract_maya_scene_raw.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Extract data as Maya scene (raw).""" import os from maya import cmds @@ -6,24 +8,37 @@ import avalon.maya import pype.api -class ExtractMayaAsciiRaw(pype.api.Extractor): - """Extract as Maya Ascii (raw) +class ExtractMayaSceneRaw(pype.api.Extractor): + """Extract as Maya Scene (raw). This will preserve all references, construction history, etc. - """ - label = "Maya ASCII (Raw)" + label = "Maya Scene (Raw)" hosts = ["maya"] families = ["mayaAscii", "setdress", "layout"] + scene_type = "ma" def process(self, instance): - + """Plugin entry point.""" + ext_mapping = instance.context.data["presets"]["maya"].get("ext_mapping") # noqa: E501 + if ext_mapping: + self.log.info("Looking in presets for scene type ...") + # use extension mapping for first family found + for family in self.families: + try: + self.scene_type = ext_mapping[family] + self.log.info( + "Using {} as scene type".format(self.scene_type)) + break + except AttributeError: + # no preset found + pass # Define extract output file path dir_path = self.staging_dir(instance) - filename = "{0}.ma".format(instance.name) + filename = "{0}.{1}".format(instance.name, self.scene_type) path = os.path.join(dir_path, filename) # Whether to include all nodes in the instance (including those from @@ -38,12 +53,12 @@ class ExtractMayaAsciiRaw(pype.api.Extractor): members = instance[:] # Perform extraction - self.log.info("Performing extraction..") + self.log.info("Performing extraction ...") with avalon.maya.maintained_selection(): cmds.select(members, noExpand=True) cmds.file(path, force=True, - typ="mayaAscii", + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 exportSelected=True, preserveReferences=True, constructionHistory=True, @@ -55,8 +70,8 @@ class ExtractMayaAsciiRaw(pype.api.Extractor): instance.data["representations"] = [] representation = { - 'name': 'ma', - 'ext': 'ma', + 'name': self.scene_type, + 'ext': self.scene_type, 'files': filename, "stagingDir": dir_path } diff --git a/pype/plugins/maya/publish/extract_model.py b/pype/plugins/maya/publish/extract_model.py index ba56194eea..330e471e53 100644 --- a/pype/plugins/maya/publish/extract_model.py +++ b/pype/plugins/maya/publish/extract_model.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Extract model as Maya Scene.""" import os from maya import cmds @@ -8,7 +10,7 @@ from pype.hosts.maya import lib class ExtractModel(pype.api.Extractor): - """Extract as Model (Maya Ascii) + """Extract as Model (Maya Scene). Only extracts contents based on the original "setMembers" data to ensure publishing the least amount of required shapes. From that it only takes @@ -22,19 +24,33 @@ class ExtractModel(pype.api.Extractor): """ - label = "Model (Maya ASCII)" + label = "Model (Maya Scene)" hosts = ["maya"] families = ["model"] + scene_type = "ma" def process(self, instance): - + """Plugin entry point.""" + ext_mapping = instance.context.data["presets"]["maya"].get("ext_mapping") # noqa: E501 + if ext_mapping: + self.log.info("Looking in presets for scene type ...") + # use extension mapping for first family found + for family in self.families: + try: + self.scene_type = ext_mapping[family] + self.log.info( + "Using {} as scene type".format(self.scene_type)) + break + except AttributeError: + # no preset found + pass # Define extract output file path stagingdir = self.staging_dir(instance) - filename = "{0}.ma".format(instance.name) + filename = "{0}.{1}".format(instance.name, self.scene_type) path = os.path.join(stagingdir, filename) # Perform extraction - self.log.info("Performing extraction..") + self.log.info("Performing extraction ...") # Get only the shape contents we need in such a way that we avoid # taking along intermediateObjects @@ -59,7 +75,7 @@ class ExtractModel(pype.api.Extractor): cmds.select(members, noExpand=True) cmds.file(path, force=True, - typ="mayaAscii", + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 exportSelected=True, preserveReferences=False, channels=False, @@ -73,8 +89,8 @@ class ExtractModel(pype.api.Extractor): instance.data["representations"] = [] representation = { - 'name': 'ma', - 'ext': 'ma', + 'name': self.scene_type, + 'ext': self.scene_type, 'files': filename, "stagingDir": stagingdir, } diff --git a/pype/plugins/maya/publish/extract_rig.py b/pype/plugins/maya/publish/extract_rig.py index c98e562313..8ebeae4184 100644 --- a/pype/plugins/maya/publish/extract_rig.py +++ b/pype/plugins/maya/publish/extract_rig.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Extract rig as Maya Scene.""" import os from maya import cmds @@ -7,26 +9,40 @@ import pype.api class ExtractRig(pype.api.Extractor): - """Extract rig as Maya Ascii""" + """Extract rig as Maya Scene.""" - label = "Extract Rig (Maya ASCII)" + label = "Extract Rig (Maya Scene)" hosts = ["maya"] families = ["rig"] + scene_type = "ma" def process(self, instance): - + """Plugin entry point.""" + ext_mapping = instance.context.data["presets"]["maya"].get("ext_mapping") # noqa: E501 + if ext_mapping: + self.log.info("Looking in presets for scene type ...") + # use extension mapping for first family found + for family in self.families: + try: + self.scene_type = ext_mapping[family] + self.log.info( + "Using {} as scene type".format(self.scene_type)) + break + except AttributeError: + # no preset found + pass # Define extract output file path dir_path = self.staging_dir(instance) - filename = "{0}.ma".format(instance.name) + filename = "{0}.{1}".format(instance.name, self.scene_type) path = os.path.join(dir_path, filename) # Perform extraction - self.log.info("Performing extraction..") + self.log.info("Performing extraction ...") with avalon.maya.maintained_selection(): cmds.select(instance, noExpand=True) cmds.file(path, force=True, - typ="mayaAscii", + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 exportSelected=True, preserveReferences=False, channels=True, @@ -38,12 +54,11 @@ class ExtractRig(pype.api.Extractor): instance.data["representations"] = [] representation = { - 'name': 'ma', - 'ext': 'ma', + 'name': self.scene_type, + 'ext': self.scene_type, 'files': filename, "stagingDir": dir_path } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) diff --git a/pype/plugins/maya/publish/extract_yeti_rig.py b/pype/plugins/maya/publish/extract_yeti_rig.py index d7bbcd6555..2f66d3e026 100644 --- a/pype/plugins/maya/publish/extract_yeti_rig.py +++ b/pype/plugins/maya/publish/extract_yeti_rig.py @@ -1,3 +1,6 @@ +# -*- coding: utf-8 -*- +"""Extract Yeti rig.""" + import os import json import contextlib @@ -11,7 +14,7 @@ import pype.hosts.maya.lib as maya @contextlib.contextmanager def disconnect_plugs(settings, members): - + """Disconnect and store attribute connections.""" members = cmds.ls(members, long=True) original_connections = [] try: @@ -55,7 +58,7 @@ def disconnect_plugs(settings, members): @contextlib.contextmanager def yetigraph_attribute_values(assumed_destination, resources): - + """Get values from Yeti attributes in graph.""" try: for resource in resources: if "graphnode" not in resource: @@ -89,14 +92,28 @@ def yetigraph_attribute_values(assumed_destination, resources): class ExtractYetiRig(pype.api.Extractor): - """Extract the Yeti rig to a MayaAscii and write the Yeti rig data""" + """Extract the Yeti rig to a Maya Scene and write the Yeti rig data.""" label = "Extract Yeti Rig" hosts = ["maya"] families = ["yetiRig"] + scene_type = "ma" def process(self, instance): - + """Plugin entry point.""" + ext_mapping = instance.context.data["presets"]["maya"].get("ext_mapping") # noqa: E501 + if ext_mapping: + self.log.info("Looking in presets for scene type ...") + # use extension mapping for first family found + for family in self.families: + try: + self.scene_type = ext_mapping[family] + self.log.info( + "Using {} as scene type".format(self.scene_type)) + break + except AttributeError: + # no preset found + pass yeti_nodes = cmds.ls(instance, type="pgYetiMaya") if not yeti_nodes: raise RuntimeError("No pgYetiMaya nodes found in the instance") @@ -106,7 +123,8 @@ class ExtractYetiRig(pype.api.Extractor): settings_path = os.path.join(dirname, "yeti.rigsettings") # Yeti related staging dirs - maya_path = os.path.join(dirname, "yeti_rig.ma") + maya_path = os.path.join( + dirname, "yeti_rig.{}".format(self.scene_type)) self.log.info("Writing metadata file") @@ -153,7 +171,7 @@ class ExtractYetiRig(pype.api.Extractor): cmds.file(maya_path, force=True, exportSelected=True, - typ="mayaAscii", + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 preserveReferences=False, constructionHistory=True, shader=False) @@ -163,21 +181,21 @@ class ExtractYetiRig(pype.api.Extractor): if "representations" not in instance.data: instance.data["representations"] = [] - self.log.info("rig file: {}".format("yeti_rig.ma")) + self.log.info("rig file: {}".format(maya_path)) instance.data["representations"].append( { - 'name': "ma", - 'ext': 'ma', - 'files': "yeti_rig.ma", + 'name': self.scene_type, + 'ext': self.scene_type, + 'files': os.path.basename(maya_path), 'stagingDir': dirname } ) - self.log.info("settings file: {}".format("yeti.rigsettings")) + self.log.info("settings file: {}".format(settings)) instance.data["representations"].append( { 'name': 'rigsettings', 'ext': 'rigsettings', - 'files': 'yeti.rigsettings', + 'files': os.path.basename(settings), 'stagingDir': dirname } ) From 941f6be192f46e7ba8c9042e61b73bf7875c0663 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 7 Aug 2020 12:39:47 +0200 Subject: [PATCH 33/33] query also avalon group in get_pype_attr --- pype/modules/ftrack/lib/avalon_sync.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index f78e617f3a..3548968628 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -81,10 +81,11 @@ def check_regex(name, entity_type, in_schema=None, schema_patterns=None): def get_pype_attr(session, split_hierarchical=True): custom_attributes = [] hier_custom_attributes = [] + # TODO remove deprecated "avalon" group from query cust_attrs_query = ( "select id, entity_type, object_type_id, is_hierarchical, default" " from CustomAttributeConfiguration" - " where group.name = \"{}\"" + " where group.name in (\"{}\", \"avalon\")" ).format(CUST_ATTR_GROUP) all_avalon_attr = session.query(cust_attrs_query).all() for cust_attr in all_avalon_attr: