From ce656a20dd82e74105c3af2e1941c842986692e7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 28 Jun 2019 17:16:21 +0200 Subject: [PATCH 01/10] created event that synchronize hierarchical custom attributes --- pype/ftrack/events/event_sync_hier_attr.py | 149 +++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 pype/ftrack/events/event_sync_hier_attr.py diff --git a/pype/ftrack/events/event_sync_hier_attr.py b/pype/ftrack/events/event_sync_hier_attr.py new file mode 100644 index 0000000000..8114e25eee --- /dev/null +++ b/pype/ftrack/events/event_sync_hier_attr.py @@ -0,0 +1,149 @@ +import os +import sys + +from pype.ftrack.lib.custom_db_connector import DbConnector + +from pype.vendor import ftrack_api +from pype.ftrack import BaseEvent, lib +from bson.objectid import ObjectId + + +class SyncHierarchicalAttrs(BaseEvent): + # After sync to avalon event! + priority = 101 + db_con = lib.custom_db_connector.DbConnector( + mongo_url=os.environ["AVALON_MONGO"], + database_name=os.environ["AVALON_DB"], + table_name=None + ) + def launch(self, session, event): + self.project_name = None + # Filter entities and changed values if it makes sence to run script + processable = [] + for ent in event['data']['entities']: + keys = ent.get('keys') + if not keys: + continue + processable.append(ent) + + if not processable: + return True + + custom_attributes = {} + query = 'CustomAttributeGroup where name is "avalon"' + all_avalon_attr = session.query(query).one() + for cust_attr in all_avalon_attr['custom_attribute_configurations']: + if 'avalon_' in cust_attr['key']: + continue + if not cust_attr['is_hierarchical']: + continue + custom_attributes[cust_attr['key']] = cust_attr + + if not custom_attributes: + return True + + for ent in processable: + for key in ent['keys']: + if key not in custom_attributes: + continue + + entity_query = '{} where id is "{}"'.format( + ent['entity_type'], ent['entityId'] + ) + entity = self.session.query(entity_query).one() + attr_value = entity['custom_attributes'][key] + if not self.project_name: + # TODO this is not 100% sure way + if entity.entity_type.lower() == 'project': + self.project_name = entity['full_name'] + else: + self.project_name = entity['project']['full_name'] + if not self.project_name: + continue + self.db_con.active_table = self.project_name + self.db_con.install() + # TODO rewrite? + # ------DRY is not here :/------- + ca_mongoid = lib.get_ca_mongoid() + custom_attributes = entity.get('custom_attributes') + if not custom_attributes: + continue + + mongoid = custom_attributes.get(ca_mongoid) + if not mongoid: + continue + + try: + mongoid = ObjectId(mongoid) + except Exception: + continue + + mongo_entity = self.db_con.find_one({'_id': mongoid}) + if not mongo_entity: + continue + + data = mongo_entity.get('data') or {} + cur_value = data.get(key) + if cur_value: + if cur_value == attr_value: + continue + + data[key] = attr_value + self.db_con.update_one( + {'_id': mongoid}, + {'$set': {'data': data}} + ) + # ------------- + self.update_hierarchical_attribute(entity, key, attr_value) + + self.db_con.uninstall() + + return True + + def update_hierarchical_attribute(self, entity, key, value): + ca_mongoid = lib.get_ca_mongoid() + + for child in entity.get('children', []): + custom_attributes = child.get('custom_attributes') + if not custom_attributes: + continue + + child_value = custom_attributes.get(key) + + if child_value is not None: + if child_value != value: + continue + mongoid = custom_attributes.get(ca_mongoid) + if not mongoid: + continue + + try: + mongoid = ObjectId(mongoid) + except Exception: + continue + + mongo_entity = self.db_con.find_one({'_id': mongoid}) + if not mongo_entity: + continue + + data = mongo_entity.get('data') or {} + cur_value = data.get(key) + if cur_value: + if cur_value == value: + continue + + data[key] = value + self.db_con.update_one( + {'_id': mongoid}, + {'$set': {'data': data}} + ) + + self.update_hierarchical_attribute(child, key, value) + + +def register(session, **kw): + '''Register plugin. Called when used as an plugin.''' + if not isinstance(session, ftrack_api.session.Session): + return + + SyncHierarchicalAttrs(session).register() From 00ce72e8f22ddfcad42a5f1072e82d4b796f28b2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 28 Jun 2019 17:17:25 +0200 Subject: [PATCH 02/10] added custom db connector from another branch not final!!! --- pype/ftrack/lib/custom_db_connector.py | 207 +++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 pype/ftrack/lib/custom_db_connector.py diff --git a/pype/ftrack/lib/custom_db_connector.py b/pype/ftrack/lib/custom_db_connector.py new file mode 100644 index 0000000000..505ac96610 --- /dev/null +++ b/pype/ftrack/lib/custom_db_connector.py @@ -0,0 +1,207 @@ +""" +Wrapper around interactions with the database + +Copy of io module in avalon-core. + - In this case not working as singleton with api.Session! +""" + +import os +import time +import errno +import shutil +import logging +import tempfile +import functools +import contextlib + +import requests + +# Third-party dependencies +import pymongo +from pymongo.client_session import ClientSession + +def auto_reconnect(func): + """Handling auto reconnect in 3 retry times""" + @functools.wraps(func) + def decorated(*args, **kwargs): + object = args[0] + for retry in range(3): + try: + return func(*args, **kwargs) + except pymongo.errors.AutoReconnect: + object.log.error("Reconnecting..") + time.sleep(0.1) + else: + raise + + return decorated + + +class DbConnector: + + log = logging.getLogger(__name__) + timeout = 1000 + + def __init__(self, mongo_url, database_name, table_name): + self._mongo_client = None + self._sentry_client = None + self._sentry_logging_handler = None + self._database = None + self._is_installed = False + self._mongo_url = mongo_url + self._database_name = database_name + + self.active_table = table_name + + def install(self): + """Establish a persistent connection to the database""" + if self._is_installed: + return + + logging.basicConfig() + + self._mongo_client = pymongo.MongoClient( + self._mongo_url, + serverSelectionTimeoutMS=self.timeout + ) + + for retry in range(3): + try: + t1 = time.time() + self._mongo_client.server_info() + except Exception: + self.log.error("Retrying..") + time.sleep(1) + else: + break + + else: + raise IOError( + "ERROR: Couldn't connect to %s in " + "less than %.3f ms" % (self._mongo_url, timeout) + ) + + self.log.info("Connected to %s, delay %.3f s" % ( + self._mongo_url, time.time() - t1 + )) + + self._database = self._mongo_client[self._database_name] + self._is_installed = True + + def uninstall(self): + """Close any connection to the database""" + + try: + self._mongo_client.close() + except AttributeError: + pass + + self._mongo_client = None + self._database = None + self._is_installed = False + + def tables(self): + """List available tables + Returns: + list of table names + """ + collection_names = self.collections() + for table_name in collection_names: + if table_name in ("system.indexes",): + continue + yield table_name + + @auto_reconnect + def collections(self): + return self._database.collection_names() + + @auto_reconnect + def insert_one(self, item, session=None): + assert isinstance(item, dict), "item must be of type " + return self._database[self.active_table].insert_one( + item, + session=session + ) + + @auto_reconnect + def insert_many(self, items, ordered=True, session=None): + # check if all items are valid + assert isinstance(items, list), "`items` must be of type " + for item in items: + assert isinstance(item, dict), "`item` must be of type " + + return self._database[self.active_table].insert_many( + items, + ordered=ordered, + session=session + ) + + @auto_reconnect + def find(self, filter, projection=None, sort=None, session=None): + return self._database[self.active_table].find( + filter=filter, + projection=projection, + sort=sort, + session=session + ) + + @auto_reconnect + def find_one(self, filter, projection=None, sort=None, session=None): + assert isinstance(filter, dict), "filter must be " + + return self._database[self.active_table].find_one( + filter=filter, + projection=projection, + sort=sort, + session=session + ) + + @auto_reconnect + def replace_one(self, filter, replacement, session=None): + return self._database[self.active_table].replace_one( + filter, replacement, + session=session + ) + + @auto_reconnect + def update_one(self, filter, update, session=None): + return self._database[self.active_table].update_one( + filter, update, + session=session + ) + + @auto_reconnect + def update_many(self, filter, update, session=None): + return self._database[self.active_table].update_many( + filter, update, + session=session + ) + + @auto_reconnect + def distinct(self, *args, **kwargs): + return self._database[self.active_table].distinct( + *args, **kwargs + ) + + @auto_reconnect + def drop_collection(self, name_or_collection, session=None): + return self._database[self.active_table].drop( + name_or_collection, + session=session + ) + + @auto_reconnect + def delete_one(filter, collation=None, session=None): + return self._database[self.active_table].delete_one( + filter, + collation=collation, + session=session + ) + + @auto_reconnect + def delete_many(filter, collation=None, session=None): + return self._database[self.active_table].delete_many( + filter, + collation=collation, + session=session + ) From 5e502be2374f71414d5cdfc59ec1a8747e45d9f2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 28 Jun 2019 17:17:38 +0200 Subject: [PATCH 03/10] set priority on sync to avalon event --- pype/ftrack/events/event_sync_to_avalon.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/ftrack/events/event_sync_to_avalon.py b/pype/ftrack/events/event_sync_to_avalon.py index 9dd7355d5e..f6b2b48a1f 100644 --- a/pype/ftrack/events/event_sync_to_avalon.py +++ b/pype/ftrack/events/event_sync_to_avalon.py @@ -4,6 +4,8 @@ from pype.ftrack import BaseEvent, lib class Sync_to_Avalon(BaseEvent): + priority = 100 + ignore_entityType = [ 'assetversion', 'job', 'user', 'reviewsessionobject', 'timer', 'socialfeed', 'socialnotification', 'timelog' From 633b01e3c64d9708e9c214b1ef89ac029e4d6e54 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 28 Jun 2019 17:18:13 +0200 Subject: [PATCH 04/10] sync to avalon skipping hierarchical attributes --- pype/ftrack/lib/avalon_sync.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/ftrack/lib/avalon_sync.py b/pype/ftrack/lib/avalon_sync.py index 030b0b5b6c..f67b39d076 100644 --- a/pype/ftrack/lib/avalon_sync.py +++ b/pype/ftrack/lib/avalon_sync.py @@ -359,6 +359,10 @@ def get_data(entity, session, custom_attributes): data['entityType'] = entity_type for cust_attr in custom_attributes: + # skip hierarchical attributes + if cust_attr.get('is_hierarchical', False): + continue + key = cust_attr['key'] if cust_attr['entity_type'].lower() in ['asset']: data[key] = entity['custom_attributes'][key] From 7038387ba34f5cfc20da22a503d7e8243c8d57c3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 28 Jun 2019 17:18:57 +0200 Subject: [PATCH 05/10] avalon entities `data` are not overriden but updated (due to hierarchical attributes) --- pype/ftrack/lib/avalon_sync.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/pype/ftrack/lib/avalon_sync.py b/pype/ftrack/lib/avalon_sync.py index f67b39d076..d15c4f4d65 100644 --- a/pype/ftrack/lib/avalon_sync.py +++ b/pype/ftrack/lib/avalon_sync.py @@ -132,13 +132,22 @@ def import_to_avalon( entity, session, custom_attributes ) + cur_data = av_project.get('data') or {} + + enter_data = {} + for k, v in cur_data.items(): + enter_data[k] = v + for k, v in data.items(): + enter_data[k] = v + database[project_name].update_many( {'_id': ObjectId(projectId)}, {'$set': { 'name': project_name, 'config': config, - 'data': data, - }}) + 'data': data + }} + ) entity['custom_attributes'][ca_mongoid] = str(projectId) session.commit() @@ -293,6 +302,18 @@ def import_to_avalon( output['errors'] = errors return output + avalon_asset = database[project_name].find_one( + {'_id': ObjectId(mongo_id)} + ) + + cur_data = avalon_asset.get('data') or {} + + enter_data = {} + for k, v in cur_data.items(): + enter_data[k] = v + for k, v in data.items(): + enter_data[k] = v + database[project_name].update_many( {'_id': ObjectId(mongo_id)}, {'$set': { From 5352422d773ca5b131f79fe34be8f1f4c00543e9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 1 Jul 2019 22:37:58 +0200 Subject: [PATCH 06/10] simplified sync and removed custom db connector --- pype/ftrack/events/event_sync_hier_attr.py | 108 ++++------- pype/ftrack/lib/custom_db_connector.py | 207 --------------------- 2 files changed, 34 insertions(+), 281 deletions(-) delete mode 100644 pype/ftrack/lib/custom_db_connector.py diff --git a/pype/ftrack/events/event_sync_hier_attr.py b/pype/ftrack/events/event_sync_hier_attr.py index 8114e25eee..6273d302fe 100644 --- a/pype/ftrack/events/event_sync_hier_attr.py +++ b/pype/ftrack/events/event_sync_hier_attr.py @@ -1,7 +1,7 @@ import os import sys -from pype.ftrack.lib.custom_db_connector import DbConnector +from avalon.tools.libraryloader.io_nonsingleton import DbConnector from pype.vendor import ftrack_api from pype.ftrack import BaseEvent, lib @@ -11,11 +11,9 @@ from bson.objectid import ObjectId class SyncHierarchicalAttrs(BaseEvent): # After sync to avalon event! priority = 101 - db_con = lib.custom_db_connector.DbConnector( - mongo_url=os.environ["AVALON_MONGO"], - database_name=os.environ["AVALON_DB"], - table_name=None - ) + db_con = DbConnector() + ca_mongoid = lib.get_ca_mongoid() + def launch(self, session, event): self.project_name = None # Filter entities and changed values if it makes sence to run script @@ -60,40 +58,9 @@ class SyncHierarchicalAttrs(BaseEvent): self.project_name = entity['project']['full_name'] if not self.project_name: continue - self.db_con.active_table = self.project_name self.db_con.install() - # TODO rewrite? - # ------DRY is not here :/------- - ca_mongoid = lib.get_ca_mongoid() - custom_attributes = entity.get('custom_attributes') - if not custom_attributes: - continue + self.db_con.Session['AVALON_PROJECT'] = self.project_name - mongoid = custom_attributes.get(ca_mongoid) - if not mongoid: - continue - - try: - mongoid = ObjectId(mongoid) - except Exception: - continue - - mongo_entity = self.db_con.find_one({'_id': mongoid}) - if not mongo_entity: - continue - - data = mongo_entity.get('data') or {} - cur_value = data.get(key) - if cur_value: - if cur_value == attr_value: - continue - - data[key] = attr_value - self.db_con.update_one( - {'_id': mongoid}, - {'$set': {'data': data}} - ) - # ------------- self.update_hierarchical_attribute(entity, key, attr_value) self.db_con.uninstall() @@ -101,44 +68,37 @@ class SyncHierarchicalAttrs(BaseEvent): return True def update_hierarchical_attribute(self, entity, key, value): - ca_mongoid = lib.get_ca_mongoid() + custom_attributes = entity.get('custom_attributes') + if not custom_attributes: + return + + mongoid = custom_attributes.get(self.ca_mongoid) + if not mongoid: + return + + try: + mongoid = ObjectId(mongoid) + except Exception: + return + + mongo_entity = self.db_con.find_one({'_id': mongoid}) + if not mongo_entity: + return + + data = mongo_entity.get('data') or {} + cur_value = data.get(key) + if cur_value: + if cur_value == attr_value: + return + + data[key] = attr_value + self.db_con.update_one( + {'_id': mongoid}, + {'$set': {'data': data}} + ) for child in entity.get('children', []): - custom_attributes = child.get('custom_attributes') - if not custom_attributes: - continue - - child_value = custom_attributes.get(key) - - if child_value is not None: - if child_value != value: - continue - mongoid = custom_attributes.get(ca_mongoid) - if not mongoid: - continue - - try: - mongoid = ObjectId(mongoid) - except Exception: - continue - - mongo_entity = self.db_con.find_one({'_id': mongoid}) - if not mongo_entity: - continue - - data = mongo_entity.get('data') or {} - cur_value = data.get(key) - if cur_value: - if cur_value == value: - continue - - data[key] = value - self.db_con.update_one( - {'_id': mongoid}, - {'$set': {'data': data}} - ) - - self.update_hierarchical_attribute(child, key, value) + self.update_one_entity(child, key, value) def register(session, **kw): diff --git a/pype/ftrack/lib/custom_db_connector.py b/pype/ftrack/lib/custom_db_connector.py deleted file mode 100644 index 505ac96610..0000000000 --- a/pype/ftrack/lib/custom_db_connector.py +++ /dev/null @@ -1,207 +0,0 @@ -""" -Wrapper around interactions with the database - -Copy of io module in avalon-core. - - In this case not working as singleton with api.Session! -""" - -import os -import time -import errno -import shutil -import logging -import tempfile -import functools -import contextlib - -import requests - -# Third-party dependencies -import pymongo -from pymongo.client_session import ClientSession - -def auto_reconnect(func): - """Handling auto reconnect in 3 retry times""" - @functools.wraps(func) - def decorated(*args, **kwargs): - object = args[0] - for retry in range(3): - try: - return func(*args, **kwargs) - except pymongo.errors.AutoReconnect: - object.log.error("Reconnecting..") - time.sleep(0.1) - else: - raise - - return decorated - - -class DbConnector: - - log = logging.getLogger(__name__) - timeout = 1000 - - def __init__(self, mongo_url, database_name, table_name): - self._mongo_client = None - self._sentry_client = None - self._sentry_logging_handler = None - self._database = None - self._is_installed = False - self._mongo_url = mongo_url - self._database_name = database_name - - self.active_table = table_name - - def install(self): - """Establish a persistent connection to the database""" - if self._is_installed: - return - - logging.basicConfig() - - self._mongo_client = pymongo.MongoClient( - self._mongo_url, - serverSelectionTimeoutMS=self.timeout - ) - - for retry in range(3): - try: - t1 = time.time() - self._mongo_client.server_info() - except Exception: - self.log.error("Retrying..") - time.sleep(1) - else: - break - - else: - raise IOError( - "ERROR: Couldn't connect to %s in " - "less than %.3f ms" % (self._mongo_url, timeout) - ) - - self.log.info("Connected to %s, delay %.3f s" % ( - self._mongo_url, time.time() - t1 - )) - - self._database = self._mongo_client[self._database_name] - self._is_installed = True - - def uninstall(self): - """Close any connection to the database""" - - try: - self._mongo_client.close() - except AttributeError: - pass - - self._mongo_client = None - self._database = None - self._is_installed = False - - def tables(self): - """List available tables - Returns: - list of table names - """ - collection_names = self.collections() - for table_name in collection_names: - if table_name in ("system.indexes",): - continue - yield table_name - - @auto_reconnect - def collections(self): - return self._database.collection_names() - - @auto_reconnect - def insert_one(self, item, session=None): - assert isinstance(item, dict), "item must be of type " - return self._database[self.active_table].insert_one( - item, - session=session - ) - - @auto_reconnect - def insert_many(self, items, ordered=True, session=None): - # check if all items are valid - assert isinstance(items, list), "`items` must be of type " - for item in items: - assert isinstance(item, dict), "`item` must be of type " - - return self._database[self.active_table].insert_many( - items, - ordered=ordered, - session=session - ) - - @auto_reconnect - def find(self, filter, projection=None, sort=None, session=None): - return self._database[self.active_table].find( - filter=filter, - projection=projection, - sort=sort, - session=session - ) - - @auto_reconnect - def find_one(self, filter, projection=None, sort=None, session=None): - assert isinstance(filter, dict), "filter must be " - - return self._database[self.active_table].find_one( - filter=filter, - projection=projection, - sort=sort, - session=session - ) - - @auto_reconnect - def replace_one(self, filter, replacement, session=None): - return self._database[self.active_table].replace_one( - filter, replacement, - session=session - ) - - @auto_reconnect - def update_one(self, filter, update, session=None): - return self._database[self.active_table].update_one( - filter, update, - session=session - ) - - @auto_reconnect - def update_many(self, filter, update, session=None): - return self._database[self.active_table].update_many( - filter, update, - session=session - ) - - @auto_reconnect - def distinct(self, *args, **kwargs): - return self._database[self.active_table].distinct( - *args, **kwargs - ) - - @auto_reconnect - def drop_collection(self, name_or_collection, session=None): - return self._database[self.active_table].drop( - name_or_collection, - session=session - ) - - @auto_reconnect - def delete_one(filter, collation=None, session=None): - return self._database[self.active_table].delete_one( - filter, - collation=collation, - session=session - ) - - @auto_reconnect - def delete_many(filter, collation=None, session=None): - return self._database[self.active_table].delete_many( - filter, - collation=collation, - session=session - ) From f94932725077b7746b37e2e16c91246bb5f33f22 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 2 Jul 2019 09:59:36 +0200 Subject: [PATCH 07/10] fixed few code bugs --- pype/ftrack/events/event_sync_hier_attr.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pype/ftrack/events/event_sync_hier_attr.py b/pype/ftrack/events/event_sync_hier_attr.py index 6273d302fe..3af9d59f4b 100644 --- a/pype/ftrack/events/event_sync_hier_attr.py +++ b/pype/ftrack/events/event_sync_hier_attr.py @@ -88,17 +88,17 @@ class SyncHierarchicalAttrs(BaseEvent): data = mongo_entity.get('data') or {} cur_value = data.get(key) if cur_value: - if cur_value == attr_value: + if cur_value == value: return - data[key] = attr_value - self.db_con.update_one( + data[key] = value + self.db_con.update_many( {'_id': mongoid}, {'$set': {'data': data}} ) for child in entity.get('children', []): - self.update_one_entity(child, key, value) + self.update_hierarchical_attribute(child, key, value) def register(session, **kw): From 17f69c7bff08ece334b8cf0453cc56b859b0d2ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 2 Jul 2019 11:34:24 +0200 Subject: [PATCH 08/10] created working version of action for sync hierarchical attributes --- pype/ftrack/actions/action_sync_hier_attrs.py | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 pype/ftrack/actions/action_sync_hier_attrs.py diff --git a/pype/ftrack/actions/action_sync_hier_attrs.py b/pype/ftrack/actions/action_sync_hier_attrs.py new file mode 100644 index 0000000000..432cd6b493 --- /dev/null +++ b/pype/ftrack/actions/action_sync_hier_attrs.py @@ -0,0 +1,214 @@ +import os +import sys +import argparse +import logging +import collections + +from pype.vendor import ftrack_api +from pype.ftrack import BaseAction, lib +from avalon.tools.libraryloader.io_nonsingleton import DbConnector +from bson.objectid import ObjectId + + +class SyncHierarchicalAttrs(BaseAction): + + db_con = DbConnector() + ca_mongoid = lib.get_ca_mongoid() + + #: Action identifier. + identifier = 'sync.hierarchical.attrs' + #: Action label. + label = 'Sync hierarchical attributes' + #: Action description. + description = 'Synchronize hierarchical attributes' + #: Icon + icon = '{}/ftrack/action_icons/SyncHierarchicalAttrs.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') + ) + + #: roles that are allowed to register this action + role_list = ['Administrator'] + + def discover(self, session, entities, event): + ''' Validation ''' + for entity in entities: + if ( + entity['context_type'].lower() in ('show', 'task') and + entity.entity_type.lower() != 'task' + ): + return True + return False + + def launch(self, session, entities, event): + # Collect hierarchical attrs + custom_attributes = {} + all_avalon_attr = session.query( + 'CustomAttributeGroup where name is "avalon"' + ).one() + for cust_attr in all_avalon_attr['custom_attribute_configurations']: + if 'avalon_' in cust_attr['key']: + continue + + if not cust_attr['is_hierarchical']: + continue + + if cust_attr['default']: + self.log.warning(( + 'Custom attribute "{}" has set default value.' + ' This attribute can\'t be synchronized' + ).format(cust_attr['label'])) + continue + + custom_attributes[cust_attr['key']] = cust_attr + + if not custom_attributes: + msg = 'No hierarchical attributes to sync.' + self.log.debug(msg) + return { + 'success': True, + 'message': msg + } + + entity = entities[0] + if entity.entity_type.lower() == 'project': + project_name = entity['full_name'] + else: + project_name = entity['project']['full_name'] + + self.db_con.install() + self.db_con.Session['AVALON_PROJECT'] = project_name + + for entity in entities: + for key in custom_attributes: + # check if entity has that attribute + if key not in entity['custom_attributes']: + self.log.debug( + 'Hierachical attribute "{}" not found on "{}"'.format( + key, entity.get('name', entity) + ) + ) + continue + + value = self.get_hierarchical_value(key, entity) + if value is None: + self.log.warning( + 'Hierarchical attribute "{}" not set on "{}"'.format( + key, entity.get('name', entity) + ) + ) + continue + + self.update_hierarchical_attribute(entity, key, value) + + self.db_con.uninstall() + + return True + + def get_hierarchical_value(self, key, entity): + value = entity['custom_attributes'][key] + if ( + value is not None or + entity.entity_type.lower() == 'project' + ): + return value + + return self.get_hierarchical_value(key, entity['parent']) + + def update_hierarchical_attribute(self, entity, key, value): + if ( + entity['context_type'].lower() not in ('show', 'task') or + entity.entity_type.lower() == 'task' + ): + return + # collect entity's custom attributes + custom_attributes = entity.get('custom_attributes') + if not custom_attributes: + return + + mongoid = custom_attributes.get(self.ca_mongoid) + if not mongoid: + self.log.debug('Entity "{}" is not synchronized to avalon.'.format( + entity.get('name', entity) + )) + return + + try: + mongoid = ObjectId(mongoid) + except Exception: + self.log.warning('Entity "{}" has stored invalid MongoID.'.format( + entity.get('name', entity) + )) + return + # Find entity in Mongo DB + mongo_entity = self.db_con.find_one({'_id': mongoid}) + if not mongo_entity: + self.log.warning( + 'Entity "{}" is not synchronized to avalon.'.format( + entity.get('name', entity) + ) + ) + return + + # Change value if entity has set it's own + entity_value = custom_attributes[key] + if entity_value is not None: + value = entity_value + + data = mongo_entity.get('data') or {} + + data[key] = value + self.db_con.update_many( + {'_id': mongoid}, + {'$set': {'data': data}} + ) + + for child in entity.get('children', []): + self.update_hierarchical_attribute(child, key, value) + + +def register(session, **kw): + '''Register plugin. Called when used as an plugin.''' + + if not isinstance(session, ftrack_api.session.Session): + return + + SyncHierarchicalAttrs(session).register() + + +def main(arguments=None): + '''Set up logging and register action.''' + if arguments is None: + arguments = [] + + parser = argparse.ArgumentParser() + # Allow setting of logging level from arguments. + loggingLevels = {} + for level in ( + logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, + logging.ERROR, logging.CRITICAL + ): + loggingLevels[logging.getLevelName(level).lower()] = level + + parser.add_argument( + '-v', '--verbosity', + help='Set the logging output verbosity.', + choices=loggingLevels.keys(), + default='info' + ) + namespace = parser.parse_args(arguments) + + # Set up basic logging + logging.basicConfig(level=loggingLevels[namespace.verbosity]) + + session = ftrack_api.Session() + register(session) + + # Wait for events + logging.info( + 'Registered actions and listening for events. Use Ctrl-C to abort.' + ) + session.event_hub.wait() + + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) From a55410392d88a4179e8d6173525ec122a4209479 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 2 Jul 2019 11:34:35 +0200 Subject: [PATCH 09/10] added icon for new action --- res/ftrack/action_icons/SyncHierarchicalAttrs.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 res/ftrack/action_icons/SyncHierarchicalAttrs.svg diff --git a/res/ftrack/action_icons/SyncHierarchicalAttrs.svg b/res/ftrack/action_icons/SyncHierarchicalAttrs.svg new file mode 100644 index 0000000000..0c59189168 --- /dev/null +++ b/res/ftrack/action_icons/SyncHierarchicalAttrs.svg @@ -0,0 +1 @@ + \ No newline at end of file From 7416de48ed2ac50a1c46517f1d5b9031abaed6e8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 2 Jul 2019 12:01:44 +0200 Subject: [PATCH 10/10] fix: event wont synchonize projects without set autosync --- pype/ftrack/events/event_sync_hier_attr.py | 45 ++++++++++++++-------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/pype/ftrack/events/event_sync_hier_attr.py b/pype/ftrack/events/event_sync_hier_attr.py index 3af9d59f4b..da033e3e0c 100644 --- a/pype/ftrack/events/event_sync_hier_attr.py +++ b/pype/ftrack/events/event_sync_hier_attr.py @@ -15,18 +15,37 @@ class SyncHierarchicalAttrs(BaseEvent): ca_mongoid = lib.get_ca_mongoid() def launch(self, session, event): - self.project_name = None # Filter entities and changed values if it makes sence to run script processable = [] + processable_ent = {} for ent in event['data']['entities']: keys = ent.get('keys') if not keys: continue + + entity = session.get(ent['entity_type'], ent['entityId']) processable.append(ent) + processable_ent[ent['entityId']] = entity if not processable: return True + for entity in processable_ent.values(): + try: + base_proj = entity['link'][0] + except Exception: + continue + ft_project = session.get(base_proj['type'], base_proj['id']) + break + + # check if project is set to auto-sync + if ( + ft_project is None or + 'avalon_auto_sync' not in ft_project['custom_attributes'] or + ft_project['custom_attributes']['avalon_auto_sync'] is False + ): + return True + custom_attributes = {} query = 'CustomAttributeGroup where name is "avalon"' all_avalon_attr = session.query(query).one() @@ -40,27 +59,16 @@ class SyncHierarchicalAttrs(BaseEvent): if not custom_attributes: return True + self.db_con.install() + self.db_con.Session['AVALON_PROJECT'] = ft_project['full_name'] + for ent in processable: for key in ent['keys']: if key not in custom_attributes: continue - entity_query = '{} where id is "{}"'.format( - ent['entity_type'], ent['entityId'] - ) - entity = self.session.query(entity_query).one() + entity = processable_ent[ent['entityId']] attr_value = entity['custom_attributes'][key] - if not self.project_name: - # TODO this is not 100% sure way - if entity.entity_type.lower() == 'project': - self.project_name = entity['full_name'] - else: - self.project_name = entity['project']['full_name'] - if not self.project_name: - continue - self.db_con.install() - self.db_con.Session['AVALON_PROJECT'] = self.project_name - self.update_hierarchical_attribute(entity, key, attr_value) self.db_con.uninstall() @@ -98,6 +106,11 @@ class SyncHierarchicalAttrs(BaseEvent): ) for child in entity.get('children', []): + if key not in child.get('custom_attributes', {}): + continue + child_value = child['custom_attributes'][key] + if child_value is not None: + continue self.update_hierarchical_attribute(child, key, value)