diff --git a/pype/ftrack/actions/action_Apps.py b/pype/ftrack/actions/action_Apps.py index 3225724524..fe7664470a 100644 --- a/pype/ftrack/actions/action_Apps.py +++ b/pype/ftrack/actions/action_Apps.py @@ -1,67 +1,55 @@ -import os -import logging import toml -import ftrack_api +import time from ftrack_action_handler import AppAction -from avalon import io, lib +from avalon import lib from app.api import Logger +from pype import lib as pypelib log = Logger.getLogger(__name__) + def registerApp(app, session): name = app['name'] variant = "" try: variant = app['name'].split("_")[1] - except Exception as e: - log.warning("'{0}' - App 'name' and 'variant' is not separated by '_' (variant is not set)".format(app['name'])) + except Exception: + log.warning(( + '"{0}" - App "name" and "variant" is not separated by "_"' + ' (variant is not set)' + ).format(app['name'])) return abspath = lib.which_app(app['name']) - if abspath == None: - log.error("'{0}' - App don't have config toml file".format(app['name'])) + if abspath is None: + log.error( + "'{0}' - App don't have config toml file".format(app['name']) + ) return apptoml = toml.load(abspath) + ''' REQUIRED ''' executable = apptoml['executable'] - label = app['label'] - if 'ftrack_label' in apptoml: - label = apptoml['ftrack_label'] - - icon = None - ftrack_resources = "" # Path to resources here - - if 'icon' in apptoml: - icon = apptoml['ftrack_icon'] - if '{ftrack_resources}' in icon: - icon = icon.format(ftrack_resources) - - description = None - if 'description' in apptoml: - description = apptoml['description'] + ''' OPTIONAL ''' + label = apptoml.get('ftrack_label', app.get('label', name)) + icon = apptoml.get('ftrack_icon', None) + description = apptoml.get('description', None) # register action - AppAction(session, label, name, executable, variant, icon, description).register() + AppAction( + session, label, name, executable, variant, icon, description + ).register() def register(session): - # TODO AVALON_PROJECT, AVALON_ASSET, AVALON_SILO need to be set or debug from avalon - - # Get all projects from Avalon DB - try: - io.install() - projects = sorted(io.projects(), key=lambda x: x['name']) - io.uninstall() - except Exception as e: - log.error(e) + projects = pypelib.get_all_avalon_projects() apps = [] appNames = [] # Get all application from all projects for project in projects: - os.environ['AVALON_PROJECT'] = project['name'] for app in project['config']['apps']: if app['name'] not in appNames: appNames.append(app['name']) @@ -71,5 +59,6 @@ def register(session): for app in apps: try: registerApp(app, session) + time.sleep(0.05) except Exception as e: log.warning("'{0}' - not proper App ({1})".format(app['name'], e)) diff --git a/pype/ftrack/actions/action_sync_to_avalon_local.py b/pype/ftrack/actions/action_sync_to_avalon_local.py index da08cad0fc..347172acb1 100644 --- a/pype/ftrack/actions/action_sync_to_avalon_local.py +++ b/pype/ftrack/actions/action_sync_to_avalon_local.py @@ -1,17 +1,15 @@ +import os import sys import argparse import logging -import os -import ftrack_api import json -import re -from pype import lib -from ftrack_action_handler import BaseAction -from bson.objectid import ObjectId -from avalon import io, inventory +import importlib +import ftrack_api +from ftrack_action_handler import BaseAction from pype.ftrack import ftrack_utils + class SyncToAvalon(BaseAction): ''' Synchronizing data action - from Ftrack to Avalon DB @@ -54,21 +52,28 @@ class SyncToAvalon(BaseAction): #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. - icon = 'https://cdn1.iconfinder.com/data/icons/hawcons/32/699650-icon-92-inbox-download-512.png' + icon = ( + 'https://cdn1.iconfinder.com/data/icons/hawcons/32/' + '699650-icon-92-inbox-download-512.png' + ) + def __init__(self, session): + super(SyncToAvalon, self).__init__(session) + # reload utils on initialize (in case of server restart) + importlib.reload(ftrack_utils) def discover(self, session, entities, event): ''' Validation ''' - roleCheck = False + role_check = False discover = False - roleList = ['Pypeclub'] - userId = event['source']['user']['id'] - user = session.query('User where id is ' + userId).one() + role_list = ['Pypeclub'] + user_id = event['source']['user']['id'] + user = session.query('User where id is ' + user_id).one() for role in user['user_security_roles']: - if role['security_role']['name'] in roleList: - roleCheck = True - if roleCheck is True: + if role['security_role']['name'] in role_list: + role_check = True + if role_check is True: for entity in entities: if entity.entity_type.lower() not in ['task', 'assetversion']: discover = True @@ -76,7 +81,6 @@ class SyncToAvalon(BaseAction): return discover - def launch(self, session, entities, event): message = "" @@ -93,10 +97,10 @@ class SyncToAvalon(BaseAction): }) try: - self.log.info("Action <" + self.__class__.__name__ + "> is running") - self.ca_mongoid = 'avalon_mongo_id' - #TODO AVALON_PROJECTS, AVALON_ASSET, AVALON_SILO should be set up otherwise console log shows avalon debug - self.setAvalonAttributes() + self.log.info( + "Action <" + self.__class__.__name__ + "> is running" + ) + self.importable = [] # get from top entity in hierarchy all parent entities @@ -108,7 +112,7 @@ class SyncToAvalon(BaseAction): # get all child entities separately/unique for entity in entities: - self.getShotAsset(entity) + self.add_childs_to_importable(entity) # Check names: REGEX in schema/duplicates - raise error if found all_names = [] @@ -122,24 +126,55 @@ class SyncToAvalon(BaseAction): all_names.append(e['name']) if len(duplicates) > 0: - raise ValueError("Entity name duplication: {}".format(", ".join(duplicates))) + raise ValueError( + "Entity name duplication: {}".format(", ".join(duplicates)) + ) - ## ----- PROJECT ------ - # store Ftrack project- self.importable[0] must be project entity!!! - self.entityProj = self.importable[0] - # set AVALON_ env - os.environ["AVALON_PROJECT"] = self.entityProj["full_name"] - os.environ["AVALON_ASSET"] = self.entityProj["full_name"] - - self.avalon_project = None - - io.install() + # ----- PROJECT ------ + # store Ftrack project- self.importable[0] must be project entity!! + ft_project = self.importable[0] + avalon_project = ftrack_utils.get_avalon_project(ft_project) + custom_attributes = ftrack_utils.get_avalon_attr(session) # Import all entities to Avalon DB - for e in self.importable: - self.importToAvalon(session, e) + for entity in self.importable: + result = ftrack_utils.import_to_avalon( + session=session, + entity=entity, + ft_project=ft_project, + av_project=avalon_project, + custom_attributes=custom_attributes + ) - io.uninstall() + if 'errors' in result and len(result['errors']) > 0: + items = [] + for error in result['errors']: + for key, message in error.items(): + name = key.lower().replace(' ', '') + info = { + 'label': key, + 'type': 'textarea', + 'name': name, + 'value': message + } + items.append(info) + self.log.error( + '{}: {}'.format(key, message) + ) + title = 'Hey You! Few Errors were raised! (*look below*)' + + job['status'] = 'failed' + session.commit() + + self.show_interface(event, items, title) + return { + 'success': False, + 'message': "Sync to avalon FAILED" + } + + if avalon_project is None: + if 'project' in result: + avalon_project = result['project'] job['status'] = 'done' session.commit() @@ -156,9 +191,16 @@ class SyncToAvalon(BaseAction): session.commit() exc_type, exc_obj, exc_tb = sys.exc_info() fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] - log_message = "{}/{}/Line: {}".format(exc_type, fname, exc_tb.tb_lineno) - self.log.error('Error during syncToAvalon: {}'.format(log_message)) - message = 'Unexpected Error - Please check Log for more information' + log_message = "{}/{}/Line: {}".format( + exc_type, fname, exc_tb.tb_lineno + ) + self.log.error( + 'Error during syncToAvalon: {}'.format(log_message) + ) + message = ( + 'Unexpected Error' + ' - Please check Log for more information' + ) if len(message) > 0: message = "Unable to sync: {}".format(message) @@ -172,14 +214,7 @@ class SyncToAvalon(BaseAction): 'message': "Synchronization was successfull" } - def setAvalonAttributes(self): - self.custom_attributes = [] - all_avalon_attr = self.session.query('CustomAttributeGroup where name is "avalon"').one() - for cust_attr in all_avalon_attr['custom_attribute_configurations']: - if 'avalon_' not in cust_attr['key']: - self.custom_attributes.append(cust_attr) - - def getShotAsset(self, entity): + def add_childs_to_importable(self, entity): if not (entity.entity_type in ['Task']): if entity not in self.importable: self.importable.append(entity) @@ -187,116 +222,7 @@ class SyncToAvalon(BaseAction): if entity['children']: childrens = entity['children'] for child in childrens: - self.getShotAsset(child) - - def importToAvalon(self, session, entity): - # --- Begin: PUSH TO Avalon --- - - entity_type = entity.entity_type - - if entity_type.lower() in ['project']: - # Set project Config - config = ftrack_utils.get_config(entity) - # Set project template - template = lib.get_avalon_project_template_schema() - if self.ca_mongoid in entity['custom_attributes']: - try: - projectId = ObjectId(self.entityProj['custom_attributes'][self.ca_mongoid]) - self.avalon_project = io.find_one({"_id": projectId}) - except: - self.log.debug("Entity {} don't have stored entity id in ftrack".format(entity['name'])) - - if self.avalon_project is None: - self.avalon_project = io.find_one({ - "type": "project", - "name": entity["full_name"] - }) - if self.avalon_project is None: - inventory.save(entity['full_name'], config, template) - self.avalon_project = io.find_one({ - "type": "project", - "name": entity["full_name"] - }) - - elif self.avalon_project['name'] != entity['full_name']: - raise ValueError('You can\'t change name {} to {}, avalon DB won\'t work properly!'.format(self.avalon_project['name'], name)) - - data = ftrack_utils.get_data(self, entity, session,self.custom_attributes) - - # Store info about project (FtrackId) - io.update_many({ - 'type': 'project', - 'name': entity['full_name'] - }, { - '$set':{'data':data, 'config':config} - }) - - self.projectId = self.avalon_project["_id"] - if self.ca_mongoid in entity['custom_attributes']: - entity['custom_attributes'][self.ca_mongoid] = str(self.projectId) - else: - self.log.error('Custom attribute for "{}" is not created.'.format(entity['name'])) - return - - ## ----- ASSETS ------ - # Presets: - data = ftrack_utils.get_data(self, entity, session, self.custom_attributes) - - # return if entity is silo - if len(data['parents']) == 0: - return - else: - silo = data['parents'][0] - - os.environ['AVALON_SILO'] = silo - - name = entity['name'] - os.environ['AVALON_ASSET'] = name - - - # Try to find asset in current database - avalon_asset = None - if self.ca_mongoid in entity['custom_attributes']: - try: - entityId = ObjectId(entity['custom_attributes'][self.ca_mongoid]) - avalon_asset = io.find_one({"_id": entityId}) - except: - self.log.debug("Entity {} don't have stored entity id in ftrack".format(entity['name'])) - - if avalon_asset is None: - avalon_asset = io.find_one({'type': 'asset', 'name': name}) - # Create if don't exists - if avalon_asset is None: - inventory.create_asset(name, silo, data, self.projectId) - self.log.debug("Asset {} - created".format(name)) - - # Raise error if it seems to be different ent. with same name - elif (avalon_asset['data']['parents'] != data['parents'] or - avalon_asset['silo'] != silo): - raise ValueError('In Avalon DB already exists entity with name "{0}"'.format(name)) - - elif avalon_asset['name'] != entity['name']: - raise ValueError('You can\'t change name {} to {}, avalon DB won\'t work properly - please set name back'.format(avalon_asset['name'], name)) - elif avalon_asset['silo'] != silo or avalon_asset['data']['parents'] != data['parents']: - old_path = "/".join(avalon_asset['data']['parents']) - new_path = "/".join(data['parents']) - raise ValueError('You can\'t move with entities. Entity "{}" was moved from "{}" to "{}" '.format(avalon_asset['name'], old_path, new_path)) - - # Update info - io.update_many({'type': 'asset','name': name}, - {'$set':{'data':data, 'silo': silo}}) - - self.log.debug("Asset {} - updated".format(name)) - - entityId = io.find_one({'type': 'asset', 'name': name})['_id'] - ## FTRACK FEATURE - FTRACK MUST HAVE avalon_mongo_id FOR EACH ENTITY TYPE EXCEPT TASK - # Set custom attribute to avalon/mongo id of entity (parentID is last) - if self.ca_mongoid in entity['custom_attributes']: - entity['custom_attributes'][self.ca_mongoid] = str(entityId) - else: - self.log.error("Custom attribute for <{}> is not created.".format(entity['name'])) - - session.commit() + self.add_childs_to_importable(child) def register(session, **kw): diff --git a/pype/ftrack/actions/action_test.py b/pype/ftrack/actions/action_test.py index 432dd44f70..f40807a420 100644 --- a/pype/ftrack/actions/action_test.py +++ b/pype/ftrack/actions/action_test.py @@ -11,7 +11,6 @@ import re import ftrack_api from ftrack_action_handler import BaseAction from avalon import io, inventory, schema -from avalon.vendor import toml class TestAction(BaseAction): @@ -24,7 +23,6 @@ class TestAction(BaseAction): #: Action description. description = 'Test action' - def discover(self, session, entities, event): ''' Validation ''' discover = False @@ -39,36 +37,9 @@ class TestAction(BaseAction): return discover - def launch(self, session, entities, event): entity = entities[0] - - entity_type = entity.entity_type - data = {} - """ - 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_' not in cust_attr['key']: - custom_attributes.append(cust_attr) - """ - for cust_attr in custom_attributes: - if cust_attr['entity_type'].lower() in ['asset']: - data[cust_attr['key']] = entity['custom_attributes'][cust_attr['key']] - - elif cust_attr['entity_type'].lower() in ['show'] and entity_type.lower() == 'project': - data[cust_attr['key']] = entity['custom_attributes'][cust_attr['key']] - - elif cust_attr['entity_type'].lower() in ['task'] and entity_type.lower() != 'project': - # Put space between capitals (e.g. 'AssetBuild' -> 'Asset Build') - entity_type = re.sub(r"(\w)([A-Z])", r"\1 \2", entity_type) - # Get object id of entity type - ent_obj_type_id = session.query('ObjectType where name is "{}"'.format(entity_type)).one()['id'] - if cust_attr['type_id'] == ent_obj_type_id: - data[cust_attr['key']] = entity['custom_attributes'][cust_attr['key']] - return True diff --git a/pype/ftrack/actions/ft_utils.py b/pype/ftrack/actions/ft_utils.py index 4b30253f9d..f6f9bfc59b 100644 --- a/pype/ftrack/actions/ft_utils.py +++ b/pype/ftrack/actions/ft_utils.py @@ -6,6 +6,8 @@ import sys import json import base64 + +ignore_me = True # sys.path.append(os.path.dirname(os.path.dirname(__file__))) # from ftrack_kredenc.lucidity.vendor import yaml # from ftrack_kredenc import lucidity diff --git a/pype/ftrack/actions/ftrack_action_handler.py b/pype/ftrack/actions/ftrack_action_handler.py index bfa297790e..1b31f5e27d 100644 --- a/pype/ftrack/actions/ftrack_action_handler.py +++ b/pype/ftrack/actions/ftrack_action_handler.py @@ -2,19 +2,18 @@ # :copyright: Copyright (c) 2017 ftrack import os import sys -import logging -import getpass import platform import ftrack_api -import toml -from avalon import io, lib, pipeline -from avalon import session as sess +from avalon import lib import acre from pype.ftrack import ftrack_utils +from pype.ftrack import ftrack_utils from pype import api as pype +ignore_me = True + class AppAction(object): '''Custom Action base class @@ -27,7 +26,10 @@ class AppAction(object): - icon in ftrack ''' - def __init__(self, session, label, name, executable, variant=None, icon=None, description=None): + def __init__( + self, session, label, name, executable, + variant=None, icon=None, description=None + ): '''Expects a ftrack_api.Session instance''' self.log = pype.Logger.getLogger(self.__class__.__name__) @@ -54,22 +56,34 @@ class AppAction(object): '''Return current session.''' return self._session - def register(self, priority = 100): + def register(self, priority=100): '''Registers the action, subscribing the discover and launch topics.''' - self.session.event_hub.subscribe( - 'topic=ftrack.action.discover and source.user.username={0}'.format( - self.session.api_user - ), self._discover,priority=priority - ) + + discovery_subscription = ( + 'topic=ftrack.action.discover and source.user.username={0}' + ).format(self.session.api_user) self.session.event_hub.subscribe( - 'topic=ftrack.action.launch and data.actionIdentifier={0} and source.user.username={1}'.format( - self.identifier, - self.session.api_user - ), + discovery_subscription, + self._discover, + priority=priority + ) + + launch_subscription = ( + 'topic=ftrack.action.launch' + ' and data.actionIdentifier={0}' + ' and source.user.username={1}' + ).format( + self.identifier, + self.session.api_user + ) + self.session.event_hub.subscribe( + launch_subscription, self._launch ) - self.log.info("Application '{} {}' - Registered successfully".format(self.label,self.variant)) + self.log.info(( + "Application '{} {}' - Registered successfully" + ).format(self.label, self.variant)) def _discover(self, event): args = self._translate_event( @@ -81,7 +95,7 @@ class AppAction(object): ) if accepts: - self.log.info('Selection is valid') + self.log.debug('Selection is valid') return { 'items': [{ 'label': self.label, @@ -92,7 +106,7 @@ class AppAction(object): }] } else: - self.log.info('Selection is _not_ valid') + self.log.debug('Selection is _not_ valid') def discover(self, session, entities, event): '''Return true if we can handle the selected entities. @@ -113,25 +127,26 @@ class AppAction(object): entity = session.get(entity_type, entity_id) # TODO Should return False if not TASK ?!!! - if entity.entity_type != 'Task': - return False - # TODO Should return False if more than one entity is selected ?!!! - if len(entities) > 1: + if ( + len(entities) > 1 or + entity.entity_type.lower() != 'task' + ): return False - ft_project = entity['project'] if (entity.entity_type != 'Project') else entity + ft_project = entity['project'] - os.environ['AVALON_PROJECT'] = ft_project['full_name'] - io.install() - project = io.find_one({"type": "project", "name": ft_project['full_name']}) - io.uninstall() + database = ftrack_utils.get_avalon_database() + project_name = ft_project['full_name'] + avalon_project = database[project_name].find_one({ + "type": "project" + }) - if project is None: + if avalon_project is None: return False else: apps = [] - for app in project['config']['apps']: + for app in avalon_project['config']['apps']: apps.append(app['name']) if self.identifier not in apps: @@ -220,27 +235,43 @@ class AppAction(object): ''' - # TODO Delete this line - self.log.info("Action - {0} ({1}) - just started".format(self.label, self.identifier)) + self.log.info(( + "Action - {0} ({1}) - just started" + ).format(self.label, self.identifier)) entity, id = entities[0] entity = session.get(entity, id) + project_name = entity['project']['full_name'] + + database = ftrack_utils.get_avalon_database() + + # Get current environments + env_list = [ + 'AVALON_PROJECT', + 'AVALON_SILO', + 'AVALON_ASSET', + 'AVALON_TASK', + 'AVALON_APP', + 'AVALON_APP_NAME' + ] + env_origin = {} + for env in env_list: + env_origin[env] = os.environ.get(env, None) # set environments for Avalon - os.environ["AVALON_PROJECT"] = entity['project']['full_name'] + os.environ["AVALON_PROJECT"] = project_name os.environ["AVALON_SILO"] = entity['ancestors'][0]['name'] os.environ["AVALON_ASSET"] = entity['parent']['name'] os.environ["AVALON_TASK"] = entity['name'] os.environ["AVALON_APP"] = self.identifier.split("_")[0] os.environ["AVALON_APP_NAME"] = self.identifier - os.environ["FTRACK_TASKID"] = id - anatomy = pype.Anatomy - io.install() - hierarchy = io.find_one({"type": 'asset', "name": entity['parent']['name']})[ - 'data']['parents'] - io.uninstall() + hierarchy = database[project_name].find_one({ + "type": 'asset', + "name": entity['parent']['name'] + })['data']['parents'] + if hierarchy: hierarchy = os.path.join(*hierarchy) @@ -252,8 +283,12 @@ class AppAction(object): try: anatomy = anatomy.format(data) except Exception as e: - self.log.error("{0} Error in anatomy.format: {1}".format(__name__, e)) - os.environ["AVALON_WORKDIR"] = os.path.join(anatomy.work.root, anatomy.work.folder) + self.log.error( + "{0} Error in anatomy.format: {1}".format(__name__, e) + ) + os.environ["AVALON_WORKDIR"] = os.path.join( + anatomy.work.root, anatomy.work.folder + ) # collect all parents from the task parents = [] @@ -307,36 +342,37 @@ class AppAction(object): try: fp = open(execfile) except PermissionError as p: - self.log.error('Access denied on {0} - {1}'. - format(execfile, p)) + self.log.error('Access denied on {0} - {1}'.format( + execfile, p)) return { 'success': False, - 'message': "Access denied on launcher - {}". - format(execfile) + 'message': "Access denied on launcher - {}".format( + execfile) } fp.close() # check executable permission if not os.access(execfile, os.X_OK): - self.log.error('No executable permission on {}'. - format(execfile)) + self.log.error('No executable permission on {}'.format( + execfile)) return { 'success': False, - 'message': "No executable permission - {}" - .format(execfile) + 'message': "No executable permission - {}".format( + execfile) } pass else: - self.log.error('Launcher doesn\'t exist - {}'. - format(execfile)) + self.log.error('Launcher doesn\'t exist - {}'.format( + execfile)) return { 'success': False, - 'message': "Launcher doesn't exist - {}" - .format(execfile) + 'message': "Launcher doesn't exist - {}".format(execfile) } pass # Run SW if was found executable if execfile is not None: - lib.launch('/usr/bin/env', args=['bash', execfile], environment=env) + lib.launch( + '/usr/bin/env', args=['bash', execfile], environment=env + ) else: return { 'success': False, @@ -347,10 +383,9 @@ class AppAction(object): # RUN TIMER IN FTRACK username = event['source']['user']['username'] - query_user = 'User where username is "{}"'.format(username) - query_task = 'Task where id is {}'.format(entity['id']) - user = session.query(query_user).one() - task = session.query(query_task).one() + user_query = 'User where username is "{}"'.format(username) + user = session.query(user_query).one() + task = session.query('Task where id is {}'.format(entity['id'])).one() self.log.info('Starting timer for task: ' + task['name']) user.start_timer(task, force=True) @@ -375,6 +410,10 @@ class AppAction(object): msg = "Status '{}' in config wasn't found on Ftrack".format(status_name) self.log.warning(msg) + # Set origin avalon environments + for key, value in env_origin.items(): + os.environ[key] = value + return { 'success': True, 'message': "Launching {0}".format(self.label) @@ -476,7 +515,7 @@ class BaseAction(object): def reset_session(self): self.session.reset() - def register(self, priority = 100): + def register(self, priority=100): ''' Registers the action, subscribing the the discover and launch topics. - highest priority event will show last @@ -487,15 +526,21 @@ class BaseAction(object): ), self._discover, priority=priority ) + launch_subscription = ( + 'topic=ftrack.action.launch' + ' and data.actionIdentifier={0}' + ' and source.user.username={1}' + ).format( + self.identifier, + self.session.api_user + ) self.session.event_hub.subscribe( - 'topic=ftrack.action.launch and data.actionIdentifier={0} and source.user.username={1}'.format( - self.identifier, - self.session.api_user - ), + launch_subscription, self._launch ) - self.log.info("Action '{}' - Registered successfully".format(self.__class__.__name__)) + self.log.info("Action '{}' - Registered successfully".format( + self.__class__.__name__)) def _discover(self, event): args = self._translate_event( @@ -546,8 +591,10 @@ class BaseAction(object): for entity in _selection: _entities.append( ( - session.get(self._get_entity_type(entity), entity.get('entityId')) - # self._get_entity_type(entity), entity.get('entityId') + session.get( + self._get_entity_type(entity), + entity.get('entityId') + ) ) ) @@ -644,7 +691,7 @@ class BaseAction(object): ''' return None - def show_message(self, event, input_message, result = False): + def show_message(self, event, input_message, result=False): """ Shows message to user who triggered event - event - just source of user id @@ -658,10 +705,13 @@ class BaseAction(object): try: message = str(input_message) - except: + except Exception: return user_id = event['source']['user']['id'] + target = ( + 'applicationId=ftrack.client.web and user.id="{0}"' + ).format(user_id) self.session.event_hub.publish( ftrack_api.event.base.Event( topic='ftrack.action.trigger-user-interface', @@ -670,7 +720,7 @@ class BaseAction(object): success=result, message=message ), - target='applicationId=ftrack.client.web and user.id="{0}"'.format(user_id) + target=target ), on_error='ignore' ) @@ -688,13 +738,19 @@ class BaseAction(object): } elif isinstance(result, dict): - for key in ('success', 'message'): - if key in result: - continue + if 'items' in result: + items = result['items'] + if not isinstance(items, list): + raise ValueError('Invalid items format, must be list!') - raise KeyError( - 'Missing required key: {0}.'.format(key) - ) + else: + for key in ('success', 'message'): + if key in result: + continue + + raise KeyError( + 'Missing required key: {0}.'.format(key) + ) else: self.log.error( @@ -702,3 +758,26 @@ class BaseAction(object): ) return result + + def show_interface(self, event, items, title=''): + """ + Shows interface to user who triggered event + - 'items' must be list containing Ftrack interface items + """ + user_id = event['source']['user']['id'] + target = ( + 'applicationId=ftrack.client.web and user.id="{0}"' + ).format(user_id) + + self.session.event_hub.publish( + ftrack_api.event.base.Event( + topic='ftrack.action.trigger-user-interface', + data=dict( + type='widget', + items=items, + title=title + ), + target=target + ), + on_error='ignore' + ) diff --git a/pype/ftrack/events/action_sync_to_avalon.py b/pype/ftrack/events/action_sync_to_avalon.py index e305b30739..0e55c1ed70 100644 --- a/pype/ftrack/events/action_sync_to_avalon.py +++ b/pype/ftrack/events/action_sync_to_avalon.py @@ -1,16 +1,12 @@ +import os import sys import argparse import logging -import os import ftrack_api import json -import re -from pype import lib -from pype.ftrack.actions.ftrack_action_handler import BaseAction -from bson.objectid import ObjectId -from avalon import io, inventory - from pype.ftrack import ftrack_utils +from pype.ftrack.actions.ftrack_action_handler import BaseAction + class Sync_To_Avalon(BaseAction): ''' @@ -54,7 +50,10 @@ class Sync_To_Avalon(BaseAction): #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. - icon = 'https://cdn1.iconfinder.com/data/icons/hawcons/32/699650-icon-92-inbox-download-512.png' + icon = ( + 'https://cdn1.iconfinder.com/data/icons/hawcons/32/' + '699650-icon-92-inbox-download-512.png' + ) def register(self): '''Registers the action, subscribing the the discover and launch topics.''' @@ -69,8 +68,10 @@ class Sync_To_Avalon(BaseAction): ), self._launch ) - - self.log.info("Action '{}' - Registered successfully".format(self.__class__.__name__)) + msg = ( + "Action '{}' - Registered successfully" + ).format(self.__class__.__name__) + self.log.info(msg) def discover(self, session, entities, event): ''' Validation ''' @@ -91,7 +92,6 @@ class Sync_To_Avalon(BaseAction): return discover - def launch(self, session, entities, event): message = "" @@ -108,10 +108,10 @@ class Sync_To_Avalon(BaseAction): }) try: - self.log.info("Action <" + self.__class__.__name__ + "> is running") - self.ca_mongoid = 'avalon_mongo_id' - #TODO AVALON_PROJECTS, AVALON_ASSET, AVALON_SILO should be set up otherwise console log shows avalon debug - self.setAvalonAttributes() + self.log.info( + "Action <" + self.__class__.__name__ + "> is running" + ) + self.importable = [] # get from top entity in hierarchy all parent entities @@ -123,7 +123,7 @@ class Sync_To_Avalon(BaseAction): # get all child entities separately/unique for entity in entities: - self.getShotAsset(entity) + self.add_childs_to_importable(entity) # Check names: REGEX in schema/duplicates - raise error if found all_names = [] @@ -137,24 +137,55 @@ class Sync_To_Avalon(BaseAction): all_names.append(e['name']) if len(duplicates) > 0: - raise ValueError("Entity name duplication: {}".format(", ".join(duplicates))) + raise ValueError( + "Entity name duplication: {}".format(", ".join(duplicates)) + ) - ## ----- PROJECT ------ - # store Ftrack project- self.importable[0] must be project entity!!! - self.entityProj = self.importable[0] - # set AVALON_ env - os.environ["AVALON_PROJECT"] = self.entityProj["full_name"] - os.environ["AVALON_ASSET"] = self.entityProj["full_name"] - - self.avalon_project = None - - io.install() + # ----- PROJECT ------ + # store Ftrack project- self.importable[0] must be project entity!! + ft_project = self.importable[0] + avalon_project = ftrack_utils.get_avalon_project(ft_project) + custom_attributes = ftrack_utils.get_avalon_attr(session) # Import all entities to Avalon DB - for e in self.importable: - self.importToAvalon(session, e) + for entity in self.importable: + result = ftrack_utils.import_to_avalon( + session=session, + entity=entity, + ft_project=ft_project, + av_project=avalon_project, + custom_attributes=custom_attributes + ) - io.uninstall() + if 'errors' in result and len(result['errors']) > 0: + items = [] + for error in result['errors']: + for key, message in error.items(): + name = key.lower().replace(' ', '') + info = { + 'label': key, + 'type': 'textarea', + 'name': name, + 'value': message + } + items.append(info) + self.log.error( + '{}: {}'.format(key, message) + ) + title = 'Hey You! Few Errors were raised! (*look below*)' + + job['status'] = 'failed' + session.commit() + + self.show_interface(event, items, title) + return { + 'success': False, + 'message': "Sync to avalon FAILED" + } + + if avalon_project is None: + if 'project' in result: + avalon_project = result['project'] job['status'] = 'done' session.commit() @@ -171,9 +202,16 @@ class Sync_To_Avalon(BaseAction): session.commit() exc_type, exc_obj, exc_tb = sys.exc_info() fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] - log_message = "{}/{}/Line: {}".format(exc_type, fname, exc_tb.tb_lineno) - self.log.error('Error during syncToAvalon: {}'.format(log_message)) - message = 'Unexpected Error - Please check Log for more information' + log_message = "{}/{}/Line: {}".format( + exc_type, fname, exc_tb.tb_lineno + ) + self.log.error( + 'Error during syncToAvalon: {}'.format(log_message) + ) + message = ( + 'Unexpected Error' + ' - Please check Log for more information' + ) if len(message) > 0: message = "Unable to sync: {}".format(message) @@ -187,14 +225,7 @@ class Sync_To_Avalon(BaseAction): 'message': "Synchronization was successfull" } - def setAvalonAttributes(self): - self.custom_attributes = [] - all_avalon_attr = self.session.query('CustomAttributeGroup where name is "avalon"').one() - for cust_attr in all_avalon_attr['custom_attribute_configurations']: - if 'avalon_' not in cust_attr['key']: - self.custom_attributes.append(cust_attr) - - def getShotAsset(self, entity): + def add_childs_to_importable(self, entity): if not (entity.entity_type in ['Task']): if entity not in self.importable: self.importable.append(entity) @@ -202,116 +233,7 @@ class Sync_To_Avalon(BaseAction): if entity['children']: childrens = entity['children'] for child in childrens: - self.getShotAsset(child) - - def importToAvalon(self, session, entity): - # --- Begin: PUSH TO Avalon --- - - entity_type = entity.entity_type - - if entity_type.lower() in ['project']: - # Set project Config - config = ftrack_utils.get_config(entity) - # Set project template - template = lib.get_avalon_project_template_schema() - if self.ca_mongoid in entity['custom_attributes']: - try: - projectId = ObjectId(self.entityProj['custom_attributes'][self.ca_mongoid]) - self.avalon_project = io.find_one({"_id": projectId}) - except: - self.log.debug("Entity {} don't have stored entity id in ftrack".format(entity['name'])) - - if self.avalon_project is None: - self.avalon_project = io.find_one({ - "type": "project", - "name": entity["full_name"] - }) - if self.avalon_project is None: - inventory.save(entity['full_name'], config, template) - self.avalon_project = io.find_one({ - "type": "project", - "name": entity["full_name"] - }) - - elif self.avalon_project['name'] != entity['full_name']: - raise ValueError('You can\'t change name {} to {}, avalon DB won\'t work properly!'.format(self.avalon_project['name'], name)) - - data = ftrack_utils.get_data(self, entity, session,self.custom_attributes) - - # Store info about project (FtrackId) - io.update_many({ - 'type': 'project', - 'name': entity['full_name'] - }, { - '$set':{'data':data, 'config':config} - }) - - self.projectId = self.avalon_project["_id"] - if self.ca_mongoid in entity['custom_attributes']: - entity['custom_attributes'][self.ca_mongoid] = str(self.projectId) - else: - self.log.error('Custom attribute for "{}" is not created.'.format(entity['name'])) - return - - ## ----- ASSETS ------ - # Presets: - data = ftrack_utils.get_data(self, entity, session, self.custom_attributes) - - # return if entity is silo - if len(data['parents']) == 0: - return - else: - silo = data['parents'][0] - - os.environ['AVALON_SILO'] = silo - - name = entity['name'] - os.environ['AVALON_ASSET'] = name - - - # Try to find asset in current database - avalon_asset = None - if self.ca_mongoid in entity['custom_attributes']: - try: - entityId = ObjectId(entity['custom_attributes'][self.ca_mongoid]) - avalon_asset = io.find_one({"_id": entityId}) - except: - self.log.debug("Entity {} don't have stored entity id in ftrack".format(entity['name'])) - - if avalon_asset is None: - avalon_asset = io.find_one({'type': 'asset', 'name': name}) - # Create if don't exists - if avalon_asset is None: - inventory.create_asset(name, silo, data, self.projectId) - self.log.debug("Asset {} - created".format(name)) - - # Raise error if it seems to be different ent. with same name - elif (avalon_asset['data']['parents'] != data['parents'] or - avalon_asset['silo'] != silo): - raise ValueError('In Avalon DB already exists entity with name "{0}"'.format(name)) - - elif avalon_asset['name'] != entity['name']: - raise ValueError('You can\'t change name {} to {}, avalon DB won\'t work properly - please set name back'.format(avalon_asset['name'], name)) - elif avalon_asset['silo'] != silo or avalon_asset['data']['parents'] != data['parents']: - old_path = "/".join(avalon_asset['data']['parents']) - new_path = "/".join(data['parents']) - raise ValueError('You can\'t move with entities. Entity "{}" was moved from "{}" to "{}" '.format(avalon_asset['name'], old_path, new_path)) - - # Update info - io.update_many({'type': 'asset','name': name}, - {'$set':{'data':data, 'silo': silo}}) - - self.log.debug("Asset {} - updated".format(name)) - - entityId = io.find_one({'type': 'asset', 'name': name})['_id'] - ## FTRACK FEATURE - FTRACK MUST HAVE avalon_mongo_id FOR EACH ENTITY TYPE EXCEPT TASK - # Set custom attribute to avalon/mongo id of entity (parentID is last) - if self.ca_mongoid in entity['custom_attributes']: - entity['custom_attributes'][self.ca_mongoid] = str(entityId) - else: - self.log.error("Custom attribute for <{}> is not created.".format(entity['name'])) - - session.commit() + self.add_childs_to_importable(child) def register(session, **kw): diff --git a/pype/ftrack/events/event_sync_to_avalon.py b/pype/ftrack/events/event_sync_to_avalon.py index 0f2cb9d29f..25b8be4359 100644 --- a/pype/ftrack/events/event_sync_to_avalon.py +++ b/pype/ftrack/events/event_sync_to_avalon.py @@ -1,312 +1,132 @@ import os -import sys -import re import ftrack_api -from ftrack_event_handler import BaseEvent -from pype import lib -from avalon import io, inventory -from avalon.vendor import toml -from bson.objectid import ObjectId from pype.ftrack import ftrack_utils - - -class ExpectedError(Exception): - def __init__(self, *args, **kwargs): - super().__init__(self, *args, **kwargs) +from ftrack_event_handler import BaseEvent class Sync_to_Avalon(BaseEvent): def launch(self, session, entities, event): - self.ca_mongoid = 'avalon_mongo_id' + + ca_mongoid = ftrack_utils.get_ca_mongoid() # If mongo_id textfield has changed: RETURN! # - infinite loop for ent in event['data']['entities']: if 'keys' in ent: - if self.ca_mongoid in ent['keys']: + if ca_mongoid in ent['keys']: return - self.proj = None - self.errors = [] + + ft_project = None # get project for entity in entities: try: base_proj = entity['link'][0] - except: + except Exception: continue - self.proj = session.get(base_proj['type'], base_proj['id']) + ft_project = session.get(base_proj['type'], base_proj['id']) break # check if project is set to auto-sync if ( - self.proj is None or - 'avalon_auto_sync' not in self.proj['custom_attributes'] or - self.proj['custom_attributes']['avalon_auto_sync'] is False + 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 # check if project have Custom Attribute 'avalon_mongo_id' - if self.ca_mongoid not in self.proj['custom_attributes']: - message = "Custom attribute '{}' for 'Project' is not created or don't have set permissions for API".format(self.ca_mongoid) + if ca_mongoid not in ft_project['custom_attributes']: + message = ( + "Custom attribute '{}' for 'Project' is not created" + " or don't have set permissions for API" + ).format(ca_mongoid) self.log.warning(message) self.show_message(event, message, False) return - self.projectId = self.proj['custom_attributes'][self.ca_mongoid] - - os.environ["AVALON_PROJECT"] = self.proj['full_name'] - # get avalon project if possible - io.install() - try: - self.avalon_project = io.find_one({ - "_id": ObjectId(self.projectId) - }) - except: - self.avalon_project = None + import_entities = [] - importEntities = [] - if self.avalon_project is None: - self.avalon_project = io.find_one({ - "type": "project", - "name": self.proj["full_name"] - }) - if self.avalon_project is None: - importEntities.append(self.proj) - else: - self.projectId = self.avalon_project['_id'] + custom_attributes = ftrack_utils.get_avalon_attr(session) - io.uninstall() + avalon_project = ftrack_utils.get_avalon_project(ft_project) + if avalon_project is None: + import_entities.append(ft_project) for entity in entities: if entity.entity_type.lower() in ['task']: entity = entity['parent'] - if ( - 'custom_attributes' not in entity or - self.ca_mongoid not in entity['custom_attributes'] - ): - message = "Custom attribute '{}' for '{}' is not created or don't have set permissions for API".format(self.ca_mongoid, entity.entity_type) + if 'custom_attributes' not in entity: + continue + if ca_mongoid not in entity['custom_attributes']: + + message = ( + "Custom attribute '{}' for '{}' is not created" + " or don't have set permissions for API" + ).format(ca_mongoid, entity.entity_type) + self.log.warning(message) self.show_message(event, message, False) return - if entity not in importEntities: - importEntities.append(entity) + if entity not in import_entities: + import_entities.append(entity) - if len(importEntities) < 1: + if len(import_entities) < 1: return - self.setAvalonAttributes() - - io.install() try: - for entity in importEntities: - self.importToAvalon(session, event, entity) - session.commit() + for entity in import_entities: + result = ftrack_utils.import_to_avalon( + session=session, + entity=entity, + ft_project=ft_project, + av_project=avalon_project, + custom_attributes=custom_attributes + ) + if 'errors' in result and len(result['errors']) > 0: + items = [] + for error in result['errors']: + for key, message in error.items(): + name = key.lower().replace(' ', '') + info = { + 'label': key, + 'type': 'textarea', + 'name': name, + 'value': message + } + items.append(info) + self.log.error( + '{}: {}'.format(key, message) + ) + session.commit() + title = 'Hey You! You raised few Errors! (*look below*)' + self.show_interface(event, items, title) + return - except ExpectedError as ee: - items = [] - for error in self.errors: - info = { - 'label': 'Error', - 'type': 'textarea', - 'name': 'error', - 'value': error - } - items.append(info) - self.log.warning(error) - self.show_interface(event, items) + if avalon_project is None: + if 'project' in result: + avalon_project = result['project'] except Exception as e: message = str(e) - ftrack_message = "SyncToAvalon event ended with unexpected error please check log file for more information." + ftrack_message = ( + 'SyncToAvalon event ended with unexpected error' + ' please check log file for more information.' + ) items = [{ - 'label': 'Error', + 'label': 'Fatal Error', 'type': 'textarea', 'name': 'error', 'value': ftrack_message }] - self.show_interface(event, items) + title = 'Hey You! Unknown Error has been raised! (*look below*)' + self.show_interface(event, items, title) self.log.error(message) - io.uninstall() - return - def importToAvalon(self, session, event, entity): - if self.ca_mongoid not in entity['custom_attributes']: - raise ValueError("Custom attribute '{}' for '{}' is not created or don't have set permissions for API".format(self.ca_mongoid, entity['name'])) - - ftrack_utils.avalon_check_name(entity) - - entity_type = entity.entity_type - - if entity_type in ['Project']: - type = 'project' - name = entity['full_name'] - config = ftrack_utils.get_config(entity) - template = lib.get_avalon_project_template_schema() - - if self.avalon_project is None: - inventory.save(name, config, template) - self.avalon_project = io.find_one({'type': type, 'name': name}) - - elif self.avalon_project['name'] != name: - entity['name'] = self.avalon_project['name'] - session.commit() - - msg = 'You can\'t change name {} to {}, avalon wouldn\'t work properly!\nName was changed back!'.format(self.avalon_project['name'], name) - self.errors.append(msg) - return - - self.projectId = self.avalon_project['_id'] - - data = ftrack_utils.get_data(self, entity, session, self.custom_attributes) - - io.update_many( - {"_id": ObjectId(self.projectId)}, - {'$set': { - 'name': name, - 'config': config, - 'data': data, - }}) - - entity['custom_attributes'][self.ca_mongoid] = str(self.projectId) - - return - - if self.avalon_project is None: - self.importToAvalon(session, event, self.proj) - - data = ftrack_utils.get_data(self, entity, session, self.custom_attributes) - - # only check name if entity is silo - if len(data['parents']) == 0: - if self.checkSilo(entity, event, session) is False: - raise ExpectedError - return - else: - silo = data['parents'][0] - - name = entity['name'] - - os.environ["AVALON_ASSET"] = name - os.environ['AVALON_SILO'] = silo - - avalon_asset = None - # existence of this custom attr is already checked - mongo_id = entity['custom_attributes'][self.ca_mongoid] - - if mongo_id is not "": - avalon_asset = io.find_one({'_id': ObjectId(mongo_id)}) - - if avalon_asset is None: - avalon_asset = io.find_one({'type': 'asset', 'name': name}) - if avalon_asset is None: - mongo_id = inventory.create_asset(name, silo, data, ObjectId(self.projectId)) - # Raise error if it seems to be different ent. with same name - elif ( - avalon_asset['data']['parents'] != data['parents'] or - avalon_asset['silo'] != silo - ): - msg = 'In Avalon DB already exists entity with name "{0}"'.format(name) - self.errors.append(msg) - return - else: - if avalon_asset['name'] != entity['name']: - if self.checkChilds(entity) is False: - msg = 'You can\'t change name {} to {}, avalon wouldn\'t work properly!\n\nName was changed back!\n\nCreate new entity if you want to change name.'.format(avalon_asset['name'], entity['name']) - entity['name'] = avalon_asset['name'] - session.commit() - self.errors.append(msg) - - if avalon_asset['silo'] != silo or avalon_asset['data']['parents'] != data['parents']: - old_path = "/".join(avalon_asset['data']['parents']) - new_path = "/".join(data['parents']) - msg = 'You can\'t move with entities.\nEntity "{}" was moved from "{}" to "{}"\n\nAvalon won\'t work properly, please move them back!'.format(avalon_asset['name'], old_path, new_path) - self.errors.append(msg) - - if len(self.errors) > 0: - raise ExpectedError - - io.update_many( - {"_id": ObjectId(mongo_id)}, - {'$set': { - 'name': name, - 'silo': silo, - 'data': data, - 'parent': ObjectId(self.projectId)}}) - - entity['custom_attributes'][self.ca_mongoid] = str(mongo_id) - - def checkChilds(self, entity): - if (entity.entity_type.lower() != 'task' and 'children' not in entity): - return True - childs = entity['children'] - for child in childs: - if child.entity_type.lower() == 'task': - config = ftrack_utils.get_config_data() - if 'sync_to_avalon' in config: - config = config['sync_to_avalon'] - if 'statuses_name_change' in config: - available_statuses = config['statuses_name_change'] - else: - available_statuses = [] - ent_status = child['status']['name'].lower() - if ent_status not in available_statuses: - return False - # If not task go deeper - elif self.checkChilds(child) is False: - return False - # If everything is allright - return True - - def checkSilo(self, entity, event, session): - changes = event['data']['entities'][0]['changes'] - if 'name' not in changes: - return True - new_name = changes['name']['new'] - old_name = changes['name']['old'] - - if 'children' not in entity or len(entity['children']) < 1: - return True - - if self.checkChilds(entity) is True: - self.updateSilo(old_name, new_name) - return True - - new_found = 0 - old_found = 0 - for asset in io.find({'silo': new_name}): - new_found += 1 - for asset in io.find({'silo': old_name}): - old_found += 1 - - if new_found > 0 or old_found == 0: - return True - - # If any condition is possible, show error to user and change name back - msg = 'You can\'t change name {} to {}, avalon wouldn\'t work properly!\n\nName was changed back!\n\nCreate new entity if you want to change name.'.format(old_name, new_name) - self.errors.append(msg) - entity['name'] = old_name - session.commit() - - return False - - def updateSilo(self, old, new): - io.update_many( - {'silo': old}, - {'$set': {'silo': new}} - ) - - def setAvalonAttributes(self): - self.custom_attributes = [] - query = 'CustomAttributeGroup where name is "avalon"' - all_avalon_attr = self.session.query(query).one() - for cust_attr in all_avalon_attr['custom_attribute_configurations']: - if 'avalon_' not in cust_attr['key']: - self.custom_attributes.append(cust_attr) - def _launch(self, event): self.session.reset() @@ -332,7 +152,10 @@ class Sync_to_Avalon(BaseEvent): continue _entities.append( ( - session.get(self._get_entity_type(entity), entity.get('entityId')) + session.get( + self._get_entity_type(entity), + entity.get('entityId') + ) ) ) diff --git a/pype/ftrack/events/ftrack_event_handler.py b/pype/ftrack/events/ftrack_event_handler.py index 0cb53b74a9..7011a57ed7 100644 --- a/pype/ftrack/events/ftrack_event_handler.py +++ b/pype/ftrack/events/ftrack_event_handler.py @@ -140,7 +140,7 @@ class BaseEvent(object): on_error='ignore' ) - def show_interface(self, event, items): + def show_interface(self, event, items, title=''): """ Shows interface to user who triggered event - 'items' must be list containing Ftrack interface items @@ -153,7 +153,8 @@ class BaseEvent(object): topic='ftrack.action.trigger-user-interface', data=dict( type='widget', - items=items + items=items, + title=title ), target=target ), diff --git a/pype/ftrack/ftrack_run.py b/pype/ftrack/ftrack_run.py index db42c501a7..2a40419cb7 100644 --- a/pype/ftrack/ftrack_run.py +++ b/pype/ftrack/ftrack_run.py @@ -1,4 +1,3 @@ -import sys import os import json import threading @@ -6,6 +5,7 @@ import time import ftrack_api from app import style from app.vendor.Qt import QtCore, QtGui, QtWidgets + from pype.ftrack import credentials, login_dialog as login_dialog from pype.vendor.pynput import mouse, keyboard @@ -21,7 +21,6 @@ log = pype.Logger.getLogger(__name__, "ftrack") class FtrackRunner: - def __init__(self, main_parent=None, parent=None): self.parent = parent @@ -86,7 +85,9 @@ class FtrackRunner: # Actions part def start_action_server(self): if self.thread_action_server is None: - self.thread_action_server = threading.Thread(target=self.set_action_server) + self.thread_action_server = threading.Thread( + target=self.set_action_server + ) self.thread_action_server.daemon = True self.thread_action_server.start() @@ -95,7 +96,14 @@ class FtrackRunner: self.set_menu_visibility() def set_action_server(self): - self.action_server.run_server() + try: + self.action_server.run_server() + except Exception: + msg = 'Ftrack Action server crashed! Please try to start again.' + log.error(msg) + # TODO show message to user + self.bool_action_server = False + self.set_menu_visibility() def reset_action_server(self): self.stop_action_server() @@ -123,11 +131,19 @@ class FtrackRunner: # Actions - server self.smActionS = self.menu.addMenu("Action server") - self.aRunActionS = QtWidgets.QAction("Run action server", self.smActionS) + + self.aRunActionS = QtWidgets.QAction( + "Run action server", self.smActionS + ) + self.aResetActionS = QtWidgets.QAction( + "Reset action server", self.smActionS + ) + self.aStopActionS = QtWidgets.QAction( + "Stop action server", self.smActionS + ) + self.aRunActionS.triggered.connect(self.start_action_server) - self.aResetActionS = QtWidgets.QAction("Reset action server", self.smActionS) self.aResetActionS.triggered.connect(self.reset_action_server) - self.aStopActionS = QtWidgets.QAction("Stop action server", self.smActionS) self.aStopActionS.triggered.connect(self.stop_action_server) self.smActionS.addAction(self.aRunActionS) @@ -168,12 +184,19 @@ class FtrackRunner: self.start_timer_thread() def start_timer_thread(self): - if self.thread_timer is None: - self.thread_timer = FtrackEventsThread(self) - self.bool_timer_event = True - self.thread_timer.signal_timer_started.connect(self.timer_started) - self.thread_timer.signal_timer_stopped.connect(self.timer_stopped) - self.thread_timer.start() + try: + if self.thread_timer is None: + self.thread_timer = FtrackEventsThread(self) + self.bool_timer_event = True + self.thread_timer.signal_timer_started.connect( + self.timer_started + ) + self.thread_timer.signal_timer_stopped.connect( + self.timer_stopped + ) + self.thread_timer.start() + except Exception: + pass def stop_timer_thread(self): try: @@ -188,9 +211,15 @@ class FtrackRunner: def start_countdown_thread(self): if self.thread_timer_coundown is None: self.thread_timer_coundown = CountdownThread(self) - self.thread_timer_coundown.signal_show_question.connect(self.show_widget_timer) - self.thread_timer_coundown.signal_send_time.connect(self.change_count_widget) - self.thread_timer_coundown.signal_stop_timer.connect(self.timer_stop) + self.thread_timer_coundown.signal_show_question.connect( + self.show_widget_timer + ) + self.thread_timer_coundown.signal_send_time.connect( + self.change_count_widget + ) + self.thread_timer_coundown.signal_stop_timer.connect( + self.timer_stop + ) self.thread_timer_coundown.start() def stop_countdown_thread(self): @@ -255,7 +284,9 @@ class FtrackEventsThread(QtCore.QThread): def run(self): self.timer_session = ftrack_api.Session(auto_connect_event_hub=True) self.timer_session.event_hub.subscribe( - 'topic=ftrack.update and source.user.username={}'.format(self.username), + 'topic=ftrack.update and source.user.username={}'.format( + self.username + ), self.event_handler) user_query = 'User where username is "{}"'.format(self.username) @@ -273,7 +304,7 @@ class FtrackEventsThread(QtCore.QThread): try: if event['data']['entities'][0]['objectTypeId'] != 'timer': return - except: + except Exception: return new = event['data']['entities'][0]['changes']['start']['new'] @@ -301,12 +332,6 @@ class FtrackEventsThread(QtCore.QThread): def ftrack_restart_timer(self): try: - last_task = None - if "FTRACK_LAST_TASK_ID" in os.environ: - task_id = os.environ["FTRACK_LAST_TASK_ID"] - query = 'Task where id is {}'.format(task_id) - last_task = self.timer_session.query(query).one() - if (self.last_task is not None) and (self.user is not None): self.user.start_timer(self.last_task) self.timer_session.commit() @@ -386,7 +411,11 @@ class CountdownThread(QtCore.QThread): json_dict = json.load(data_file) data = json_dict['timer'] except Exception as e: - msg = 'Loading "Ftrack Config file" Failed. Please check log for more information. Times are set to default.' + msg = ( + 'Loading "Ftrack Config file" Failed.' + ' Please check log for more information.' + ' Times are set to default.' + ) log.warning("{} - {}".format(msg, str(e))) data = self.validate_timer_values(data) @@ -485,15 +514,17 @@ class StopTimer(QtWidgets.QWidget): def _main(self): self.main = QtWidgets.QVBoxLayout() - self.main.setObjectName("main") + self.main.setObjectName('main') self.form = QtWidgets.QFormLayout() self.form.setContentsMargins(10, 15, 10, 5) - self.form.setObjectName("form") + self.form.setObjectName('form') - msg_info = "You didn't work for a long time." - msg_question = "Would you like to stop Ftrack timer?" - msg_stopped = "Your Ftrack timer was stopped. Do you want to start again?" + msg_info = 'You didn\'t work for a long time.' + msg_question = 'Would you like to stop Ftrack timer?' + msg_stopped = ( + 'Your Ftrack timer was stopped. Do you want to start again?' + ) self.lbl_info = QtWidgets.QLabel(msg_info) self.lbl_info.setFont(self.font) diff --git a/pype/ftrack/ftrack_utils/__init__.py b/pype/ftrack/ftrack_utils/__init__.py new file mode 100644 index 0000000000..fb09e548e6 --- /dev/null +++ b/pype/ftrack/ftrack_utils/__init__.py @@ -0,0 +1,2 @@ +from .ftrack_utils import * +from .avalon_sync import * diff --git a/pype/ftrack/ftrack_utils/avalon_sync.py b/pype/ftrack/ftrack_utils/avalon_sync.py new file mode 100644 index 0000000000..648c119b2f --- /dev/null +++ b/pype/ftrack/ftrack_utils/avalon_sync.py @@ -0,0 +1,432 @@ +import os +import re +from pype import lib +from pype.lib import get_avalon_database +from avalon import schema +from bson.objectid import ObjectId +from pype.ftrack.ftrack_utils import ftrack_utils +from avalon.vendor import jsonschema +from app.api import Logger +ValidationError = jsonschema.ValidationError + +log = Logger.getLogger(__name__) + + +def get_ca_mongoid(): + # returns name of Custom attribute that stores mongo_id + return 'avalon_mongo_id' + + +def import_to_avalon( + session, entity, ft_project, av_project, custom_attributes +): + database = get_avalon_database() + project_name = ft_project['full_name'] + output = {} + errors = [] + + ca_mongoid = get_ca_mongoid() + # Validate if entity has custom attribute avalon_mongo_id + if ca_mongoid not in entity['custom_attributes']: + msg = ( + 'Custom attribute "{}" for "{}" is not created' + ' or don\'t have set permissions for API' + ).format(ca_mongoid, entity['name']) + errors.append({'Custom attribute error': msg}) + output['errors'] = errors + return output + + # Validate if entity name match REGEX in schema + try: + ftrack_utils.avalon_check_name(entity) + except ValidationError: + msg = '"{}" includes unsupported symbols like "dash" or "space"' + errors.append({'Unsupported character': msg}) + output['errors'] = errors + return output + + entity_type = entity.entity_type + # Project //////////////////////////////////////////////////////////////// + if entity_type in ['Project']: + type = 'project' + + config = ftrack_utils.get_config(entity) + schema.validate(config) + + av_project_code = None + if av_project is not None and 'code' in av_project['data']: + av_project_code = av_project['data']['code'] + ft_project_code = ft_project['name'] + + if av_project is None: + project_schema = lib.get_avalon_project_template_schema() + item = { + 'schema': project_schema, + 'type': type, + 'name': project_name, + 'data': dict(), + 'config': config, + 'parent': None, + } + schema.validate(item) + + database[project_name].insert_one(item) + + av_project = database[project_name].find_one( + {'type': type} + ) + + elif ( + av_project['name'] != project_name or + ( + av_project_code is not None and + av_project_code != ft_project_code + ) + ): + msg = ( + 'You can\'t change {0} "{1}" to "{2}"' + ', avalon wouldn\'t work properly!' + '\n{0} was changed back!' + ) + if av_project['name'] != project_name: + entity['full_name'] = av_project['name'] + errors.append( + {'Changed name error': msg.format( + 'Project name', av_project['name'], project_name + )} + ) + if ( + av_project_code is not None and + av_project_code != ft_project_code + ): + entity['name'] = av_project_code + errors.append( + {'Changed name error': msg.format( + 'Project code', av_project_code, ft_project_code + )} + ) + + session.commit() + + output['errors'] = errors + return output + + projectId = av_project['_id'] + + data = get_data( + entity, session, custom_attributes + ) + + database[project_name].update_many( + {'_id': ObjectId(projectId)}, + {'$set': { + 'name': project_name, + 'config': config, + 'data': data, + }}) + + entity['custom_attributes'][ca_mongoid] = str(projectId) + session.commit() + + output['project'] = av_project + + return output + + # Asset - ///////////////////////////////////////////////////////////// + if av_project is None: + result = import_to_avalon( + session, ft_project, ft_project, av_project, custom_attributes + ) + + if 'errors' in result: + output['errors'] = result['errors'] + return output + + elif 'project' not in result: + msg = 'During project import went something wrong' + errors.append({'Unexpected error': msg}) + output['errors'] = errors + return output + + av_project = result['project'] + output['project'] = result['project'] + + projectId = av_project['_id'] + data = get_data( + entity, session, custom_attributes + ) + + # 1. hierarchical entity have silo set to None + silo = None + if len(data['parents']) > 0: + silo = data['parents'][0] + + name = entity['name'] + + avalon_asset = None + # existence of this custom attr is already checked + if ca_mongoid not in entity['custom_attributes']: + msg = '"{}" don\'t have "{}" custom attribute' + errors.append({'Missing Custom attribute': msg.format( + entity_type, ca_mongoid + )}) + output['errors'] = errors + return output + + mongo_id = entity['custom_attributes'][ca_mongoid] + + if mongo_id is not '': + avalon_asset = database[project_name].find_one( + {'_id': ObjectId(mongo_id)} + ) + + if avalon_asset is None: + avalon_asset = database[project_name].find_one( + {'type': 'asset', 'name': name} + ) + if avalon_asset is None: + asset_schema = lib.get_avalon_asset_template_schema() + item = { + 'schema': asset_schema, + 'name': name, + 'silo': silo, + 'parent': ObjectId(projectId), + 'type': 'asset', + 'data': data + } + schema.validate(item) + mongo_id = database[project_name].insert_one(item).inserted_id + + # Raise error if it seems to be different ent. with same name + elif ( + avalon_asset['data']['parents'] != data['parents'] or + avalon_asset['silo'] != silo + ): + msg = ( + 'In Avalon DB already exists entity with name "{0}"' + ).format(name) + errors.append({'Entity name duplication': msg}) + output['errors'] = errors + return output + + # Store new ID (in case that asset was removed from DB) + else: + mongo_id = avalon_asset['_id'] + else: + if avalon_asset['name'] != entity['name']: + if silo is None or changeability_check_childs(entity) is False: + msg = ( + 'You can\'t change name {} to {}' + ', avalon wouldn\'t work properly!' + '\n\nName was changed back!' + '\n\nCreate new entity if you want to change name.' + ).format(avalon_asset['name'], entity['name']) + entity['name'] = avalon_asset['name'] + session.commit() + errors.append({'Changed name error': msg}) + + if ( + avalon_asset['silo'] != silo or + avalon_asset['data']['parents'] != data['parents'] + ): + old_path = '/'.join(avalon_asset['data']['parents']) + new_path = '/'.join(data['parents']) + + msg = ( + 'You can\'t move with entities.' + '\nEntity "{}" was moved from "{}" to "{}"' + '\n\nAvalon won\'t work properly, {}!' + ) + + moved_back = False + if 'visualParent' in avalon_asset['data']: + if silo is None: + asset_parent_id = avalon_asset['parent'] + else: + asset_parent_id = avalon_asset['data']['visualParent'] + + asset_parent = database[project_name].find_one( + {'_id': ObjectId(asset_parent_id)} + ) + ft_parent_id = asset_parent['data']['ftrackId'] + try: + entity['parent_id'] = ft_parent_id + session.commit() + msg = msg.format( + avalon_asset['name'], old_path, new_path, + 'entity was moved back' + ) + moved_back = True + + except Exception: + moved_back = False + + if moved_back is False: + msg = msg.format( + avalon_asset['name'], old_path, new_path, + 'please move it back' + ) + + errors.append({'Hierarchy change error': msg}) + + if len(errors) > 0: + output['errors'] = errors + return output + + database[project_name].update_many( + {'_id': ObjectId(mongo_id)}, + {'$set': { + 'name': name, + 'silo': silo, + 'data': data, + 'parent': ObjectId(projectId) + }}) + + entity['custom_attributes'][ca_mongoid] = str(mongo_id) + session.commit() + + return output + + +def get_avalon_attr(session): + 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_' not in cust_attr['key']: + custom_attributes.append(cust_attr) + return custom_attributes + + +def changeability_check_childs(entity): + if (entity.entity_type.lower() != 'task' and 'children' not in entity): + return True + childs = entity['children'] + for child in childs: + if child.entity_type.lower() == 'task': + config = ftrack_utils.get_config_data() + if 'sync_to_avalon' in config: + config = config['sync_to_avalon'] + if 'statuses_name_change' in config: + available_statuses = config['statuses_name_change'] + else: + available_statuses = [] + ent_status = child['status']['name'].lower() + if ent_status not in available_statuses: + return False + # If not task go deeper + elif changeability_check_childs(child) is False: + return False + # If everything is allright + return True + + +def get_data(entity, session, custom_attributes): + database = get_avalon_database() + + entity_type = entity.entity_type + + if entity_type.lower() == 'project': + ft_project = entity + elif entity_type.lower() != 'project': + ft_project = entity['project'] + av_project = get_avalon_project(ft_project) + + project_name = ft_project['full_name'] + + data = {} + data['ftrackId'] = entity['id'] + data['entityType'] = entity_type + + for cust_attr in custom_attributes: + key = cust_attr['key'] + if cust_attr['entity_type'].lower() in ['asset']: + data[key] = entity['custom_attributes'][key] + + elif ( + cust_attr['entity_type'].lower() in ['show'] and + entity_type.lower() == 'project' + ): + data[key] = entity['custom_attributes'][key] + + elif ( + cust_attr['entity_type'].lower() in ['task'] and + entity_type.lower() != 'project' + ): + # Put space between capitals (e.g. 'AssetBuild' -> 'Asset Build') + entity_type_full = re.sub(r"(\w)([A-Z])", r"\1 \2", entity_type) + # Get object id of entity type + query = 'ObjectType where name is "{}"'.format(entity_type_full) + ent_obj_type_id = session.query(query).one()['id'] + + if cust_attr['object_type_id'] == ent_obj_type_id: + data[key] = entity['custom_attributes'][key] + + if entity_type in ['Project']: + data['code'] = entity['name'] + return data + + # Get info for 'Data' in Avalon DB + tasks = [] + for child in entity['children']: + if child.entity_type in ['Task']: + tasks.append(child['name']) + + # Get list of parents without project + parents = [] + folderStruct = [] + for i in range(1, len(entity['link'])-1): + parEnt = session.get( + entity['link'][i]['type'], + entity['link'][i]['id'] + ) + parName = parEnt['name'] + folderStruct.append(parName) + parents.append(parEnt) + + parentId = None + + for parent in parents: + parentId = database[project_name].find_one( + {'type': 'asset', 'name': parName} + )['_id'] + if parent['parent'].entity_type != 'project' and parentId is None: + import_to_avalon( + session, parent, ft_project, av_project, custom_attributes + ) + parentId = database[project_name].find_one( + {'type': 'asset', 'name': parName} + )['_id'] + + hierarchy = os.path.sep.join(folderStruct) + + data['visualParent'] = parentId + data['parents'] = folderStruct + data['tasks'] = tasks + data['hierarchy'] = hierarchy + + return data + + +def get_avalon_project(ft_project): + database = get_avalon_database() + project_name = ft_project['full_name'] + ca_mongoid = get_ca_mongoid() + if ca_mongoid not in ft_project['custom_attributes']: + return None + + # try to find by Id + project_id = ft_project['custom_attributes'][ca_mongoid] + try: + avalon_project = database[project_name].find_one({ + '_id': ObjectId(project_id) + }) + except Exception: + avalon_project = None + + if avalon_project is None: + avalon_project = database[project_name].find_one({ + 'type': 'project' + }) + + return avalon_project diff --git a/pype/ftrack/ftrack_utils.py b/pype/ftrack/ftrack_utils/ftrack_utils.py similarity index 59% rename from pype/ftrack/ftrack_utils.py rename to pype/ftrack/ftrack_utils/ftrack_utils.py index 74b50f45fb..4398beab8e 100644 --- a/pype/ftrack/ftrack_utils.py +++ b/pype/ftrack/ftrack_utils/ftrack_utils.py @@ -1,14 +1,9 @@ import os -import sys -import re import json -from pprint import * -import ftrack_api from pype import lib -import avalon.io as io -import avalon.api import avalon +import avalon.api from avalon.vendor import toml, jsonschema from app.api import Logger @@ -25,80 +20,17 @@ def get_config_data(): data = json.load(data_file) except Exception as e: - msg = 'Loading "Ftrack Config file" Failed. Please check log for more information. Times are set to default.' + msg = ( + 'Loading "Ftrack Config file" Failed.' + ' Please check log for more information.' + ' Times are set to default.' + ) log.warning("{} - {}".format(msg, str(e))) return data -def get_data(parent, entity, session, custom_attributes): - entity_type = entity.entity_type - data = {} - data['ftrackId'] = entity['id'] - data['entityType'] = entity_type - - for cust_attr in custom_attributes: - key = cust_attr['key'] - if ( - cust_attr['is_hierarchical'] is True or - cust_attr['entity_type'].lower() in ['asset'] or - ( - cust_attr['entity_type'].lower() in ['show'] and - entity_type.lower() == 'project' - ) - ): - data[key] = entity['custom_attributes'][key] - - elif ( - cust_attr['entity_type'].lower() in ['task'] and - entity_type.lower() != 'project' - ): - # Put space between capitals (e.g. 'AssetBuild' -> 'Asset Build') - entity_type_full = re.sub(r"(\w)([A-Z])", r"\1 \2", entity_type) - # Get object id of entity type - ent_obj_type_id = session.query('ObjectType where name is "{}"'.format(entity_type_full)).one()['id'] - - if cust_attr['object_type_id'] == ent_obj_type_id: - data[key] = entity['custom_attributes'][key] - - if entity_type in ['Project']: - data['code'] = entity['name'] - return data - - # Get info for 'Data' in Avalon DB - tasks = [] - for child in entity['children']: - if child.entity_type in ['Task']: - tasks.append(child['name']) - - # Get list of parents without project - parents = [] - folderStruct = [] - for i in range(1, len(entity['link'])-1): - parEnt = session.get(entity['link'][i]['type'], entity['link'][i]['id']) - parName = parEnt['name'] - folderStruct.append(parName) - if i > 1: - parents.append(parEnt) - - parentId = None - - for parent in parents: - parentId = io.find_one({'type': 'asset', 'name': parName})['_id'] - if parent['parent'].entity_type != 'project' and parentId is None: - parent.importToAvalon(session, parent) - parentId = io.find_one({'type': 'asset', 'name': parName})['_id'] - - hierarchy = os.path.sep.join(folderStruct) - - data['visualParent'] = parentId - data['parents'] = folderStruct - data['tasks'] = tasks - data['hierarchy'] = hierarchy - - return data - -def avalon_check_name(entity, inSchema = None): +def avalon_check_name(entity, inSchema=None): ValidationError = jsonschema.ValidationError alright = True name = entity['name'] @@ -113,7 +45,7 @@ def avalon_check_name(entity, inSchema = None): if entity.entity_type in ['Project']: # data['type'] = 'project' name = entity['full_name'] - # schema = get_avalon_project_template_schema()['schema'] + # schema = get_avalon_project_template_schema() # elif entity.entity_type in ['AssetBuild','Library']: # data['silo'] = 'Assets' # else: @@ -130,8 +62,8 @@ def avalon_check_name(entity, inSchema = None): alright = False if alright is False: - raise ValueError("{} includes unsupported symbols like 'dash' or 'space'".format(name)) - + msg = '"{}" includes unsupported symbols like "dash" or "space"' + raise ValueError(msg.format(name)) def get_apps(entity): @@ -156,6 +88,7 @@ def get_apps(entity): log.warning('Error with application {0} - {1}'.format(app, e)) return apps + def get_config(entity): config = {} config['schema'] = lib.get_avalon_project_config_schema() @@ -165,35 +98,8 @@ def get_config(entity): return config -def checkRegex(): - # _handle_result -> would be solution? - # """ TODO Check if name of entities match REGEX""" - for entity in importable: - for e in entity['link']: - item = { - "silo": "silo", - "parent": "parent", - "type": "asset", - "schema": "avalon-core:asset-2.0", - "name": e['name'], - "data": dict(), - } - try: - schema.validate(item) - except Exception as e: - print(e) - print(e['name']) - ftrack.EVENT_HUB.publishReply( - event, - data={ - 'success': False, - 'message': 'Entity name contains invalid character!' - } - ) - def get_context(entity): - parents = [] item = entity while True: @@ -211,12 +117,11 @@ def get_context(entity): } try: entityDic['type'] = entity['type']['name'] - except: + except Exception: pass ctx[entity['object_type']['name']] = entityDic - # add all parents to the context for parent in parents: tempdic = {} @@ -280,7 +185,7 @@ def get_next_task(task): if t.get('typeid') == task.get('typeid'): try: next_types = types_sorted[(types_sorted.index(t) + 1):] - except: + except Exception: pass for nt in next_types: diff --git a/pype/lib.py b/pype/lib.py index b44661d33e..6c067bbeba 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -409,7 +409,7 @@ def get_avalon_project_config_schema(): def get_avalon_project_template_schema(): - schema = {"schema": "avalon-core:inventory-1.0"} + schema = "avalon-core:project-2.0" return schema @@ -433,3 +433,29 @@ def get_avalon_project_template(): proj_template['work'] = "{root}/{project}/{hierarchy}/{asset}/work/{task}" proj_template['publish'] = "{root}/{project}/{hierarchy}/{asset}/publish/{family}/{subset}/v{version}/{projectcode}_{asset}_{subset}_v{version}.{representation}" return proj_template + + +def get_avalon_asset_template_schema(): + schema = "avalon-core:asset-2.0" + return schema + + +def get_avalon_database(): + if io._database is None: + project = os.environ.get('AVALON_PROJECT', '') + asset = os.environ.get('AVALON_ASSET', '') + silo = os.environ.get('AVALON_SILO', '') + os.environ['AVALON_PROJECT'] = project + os.environ['AVALON_ASSET'] = asset + os.environ['AVALON_SILO'] = silo + io.install() + return io._database + + +def get_all_avalon_projects(): + db = get_avalon_database() + project_names = db.collection_names() + projects = [] + for name in project_names: + projects.append(db[name].find_one({'type': 'project'})) + return projects diff --git a/pype/plugins/global/publish/integrate_rendered_frames.py b/pype/plugins/global/publish/integrate_rendered_frames.py index d40abb9e02..2ee28797e7 100644 --- a/pype/plugins/global/publish/integrate_rendered_frames.py +++ b/pype/plugins/global/publish/integrate_rendered_frames.py @@ -26,8 +26,7 @@ class IntegrateFrames(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder families = ["imagesequence", "render", "write", "source"] - family_targets = [".frames", ".local", ".review", - "imagesequence", "render", "source"] + family_targets = [".frames", ".local", ".review", "imagesequence", "render", "source"] exclude_families = ["clip"] def process(self, instance): diff --git a/pype/plugins/nuke/create/create_read.py b/pype/plugins/nuke/create/create_read.py new file mode 100644 index 0000000000..8597737167 --- /dev/null +++ b/pype/plugins/nuke/create/create_read.py @@ -0,0 +1,55 @@ +from collections import OrderedDict +import avalon.api +import avalon.nuke +from pype import api as pype + +import nuke + + +log = pype.Logger.getLogger(__name__, "nuke") + + +class CrateRead(avalon.nuke.Creator): + # change this to template preset + name = "ReadCopy" + label = "Create Read Copy" + hosts = ["nuke"] + family = "source" + families = family + icon = "film" + + def __init__(self, *args, **kwargs): + super(CrateRead, self).__init__(*args, **kwargs) + + data = OrderedDict() + data['family'] = self.family + data['families'] = self.families + {data.update({k: v}) for k, v in self.data.items() + if k not in data.keys()} + self.data = data + + def process(self): + self.name = self.data["subset"] + + nodes = nuke.selectedNodes() + + if not nodes or len(nodes) == 0: + nuke.message('Please select Read node') + else: + count_reads = 0 + for node in nodes: + if node.Class() != 'Read': + continue + name = node["name"].value() + avalon_data = self.data + avalon_data['subset'] = "{}_{}".format(self.family, name) + self.change_read_node(self.data["subset"], node, avalon_data) + count_reads += 1 + + if count_reads < 1: + nuke.message('Please select Read node') + return + + def change_read_node(self, name, node, data): + node = avalon.nuke.lib.imprint(node, data) + node['tile_color'].setValue(16711935) diff --git a/pype/plugins/nuke/publish/collect_families.py b/pype/plugins/nuke/publish/collect_families.py index 226df3b168..d0e61c349b 100644 --- a/pype/plugins/nuke/publish/collect_families.py +++ b/pype/plugins/nuke/publish/collect_families.py @@ -12,16 +12,12 @@ class CollectInstanceFamilies(pyblish.api.ContextPlugin): def process(self, context): for instance in context.data["instances"]: - if not instance.data["publish"]: - continue - - # set for ftrack to accept - instance.data["families"] = ["ftrack"] - if "write" in instance.data["family"]: - node = instance[0] + # set for ftrack to accept + instance.data["families"] = ["ftrack"] + if not node["render"].value(): families = ["{}.frames".format( instance.data["avalonKnob"]["families"])] @@ -38,6 +34,12 @@ class CollectInstanceFamilies(pyblish.api.ContextPlugin): instance.data["families"].extend(families) + elif "source" in instance.data["family"]: + families = [] + families.append(instance.data["avalonKnob"]["families"]) + + instance.data["families"] = families + # Sort/grouped by family (preserving local index) context[:] = sorted(context, key=self.sort_by_family) diff --git a/pype/plugins/nuke/publish/collect_instances.py b/pype/plugins/nuke/publish/collect_instances.py index 14e12f8670..33e6d5a608 100644 --- a/pype/plugins/nuke/publish/collect_instances.py +++ b/pype/plugins/nuke/publish/collect_instances.py @@ -56,6 +56,8 @@ class CollectNukeInstances(pyblish.api.ContextPlugin): "fps": int(nuke.root()['fps'].value()) }) + if node.Class() == "Write": + instance.data["families"] = [avalon_knob_data["families"]] self.log.info("collected instance: {}".format(instance.data)) instances.append(instance) diff --git a/pype/plugins/nuke/publish/collect_reads.py b/pype/plugins/nuke/publish/collect_reads.py new file mode 100644 index 0000000000..f5d3008b40 --- /dev/null +++ b/pype/plugins/nuke/publish/collect_reads.py @@ -0,0 +1,103 @@ +import os +import re +import clique +import nuke +import pyblish.api +import logging +from avalon import io, api + +log = logging.getLogger(__name__) + + +@pyblish.api.log +class CollectNukeReads(pyblish.api.ContextPlugin): + """Collect all read nodes.""" + + order = pyblish.api.CollectorOrder + 0.1 + label = "Collect Reads" + hosts = ["nuke"] + + def process(self, context): + asset_data = io.find_one({"type": "asset", + "name": api.Session["AVALON_ASSET"]}) + self.log.debug("asset_data: {}".format(asset_data["data"])) + for instance in context.data["instances"]: + self.log.debug("checking instance: {}".format(instance)) + + node = instance[0] + if node.Class() != "Read": + continue + + file_path = node["file"].value() + file_name = os.path.basename(file_path) + items = file_name.split(".") + + if len(items) < 2: + raise ValueError + + ext = items[-1] + + # # Get frame range + first_frame = node['first'].value() + last_frame = node['last'].value() + + # # Easier way to sequence - Not tested + # isSequence = True + # if first_frame == last_frame: + # isSequence = False + + isSequence = False + if len(items) > 1: + sequence = items[-2] + hash_regex = re.compile(r'([#*])') + seq_regex = re.compile('[%0-9*d]') + hash_match = re.match(hash_regex, sequence) + seq_match = re.match(seq_regex, sequence) + if hash_match or seq_match: + isSequence = True + + # get source path + path = nuke.filename(node) + source_dir = os.path.dirname(path) + self.log.debug('source dir: {}'.format(source_dir)) + + if isSequence: + source_files = os.listdir(source_dir) + else: + source_files = file_name + + # Include start and end render frame in label + name = node.name() + label = "{0} ({1}-{2})".format( + name, + int(first_frame), + int(last_frame) + ) + + self.log.debug("collected_frames: {}".format(label)) + + instance.data["files"] = [source_files] + + transfer = False + if "publish" in node.knobs(): + transfer = node["publish"] + + instance.data['transfer'] = transfer + + self.log.debug("checking for error: {}".format(label)) + instance.data.update({ + "path": path, + "stagingDir": source_dir, + "ext": ext, + "label": label, + "startFrame": first_frame, + "endFrame": last_frame, + "colorspace": node["colorspace"].value(), + "handles": int(asset_data["data"].get("handles", 0)), + "step": 1, + "fps": int(nuke.root()['fps'].value()) + }) + + self.log.debug("instance.data: {}".format(instance.data)) + + self.log.debug("context: {}".format(context)) diff --git a/pype/plugins/nuke/publish/collect_script.py b/pype/plugins/nuke/publish/collect_script.py index 92557b2665..f0c917b449 100644 --- a/pype/plugins/nuke/publish/collect_script.py +++ b/pype/plugins/nuke/publish/collect_script.py @@ -34,6 +34,12 @@ class CollectScript(pyblish.api.ContextPlugin): first_frame = int(root["first_frame"].getValue()) last_frame = int(root["last_frame"].getValue()) + # Get format + format = root['format'].value() + resolution_width = format.width() + resolution_height = format.height() + pixel_aspect = format.pixelAspect() + # Create instance instance = context.create_instance(subset) instance.add(root) @@ -45,6 +51,9 @@ class CollectScript(pyblish.api.ContextPlugin): "name": base_name, "startFrame": first_frame, "endFrame": last_frame, + "resolution_width": resolution_width, + "resolution_height": resolution_height, + "pixel_aspect": pixel_aspect, "publish": root.knob('publish').value(), "family": family, "representation": "nk", diff --git a/pype/plugins/nuke/publish/validate_script.py b/pype/plugins/nuke/publish/validate_script.py new file mode 100644 index 0000000000..a4ec60d96d --- /dev/null +++ b/pype/plugins/nuke/publish/validate_script.py @@ -0,0 +1,106 @@ +import pyblish.api +from avalon import io + + +@pyblish.api.log +class ValidateScript(pyblish.api.InstancePlugin): + """ Validates file output. """ + + order = pyblish.api.ValidatorOrder + 0.1 + families = ["nukescript"] + label = "Check nukescript settings" + hosts = ["nuke"] + + def process(self, instance): + instance_data = instance.data + asset_name = instance_data["asset"] + + asset = io.find_one({ + "type": "asset", + "name": asset_name + }) + asset_data = asset["data"] + + # These attributes will be checked + attributes = [ + "fps", "fstart", "fend", + "resolution_width", "resolution_height", "pixel_aspect" + ] + + # Value of these attributes can be found on parents + hierarchical_attributes = ["fps"] + + missing_attributes = [] + asset_attributes = {} + for attr in attributes: + if attr in asset_data: + asset_attributes[attr] = asset_data[attr] + + elif attr in hierarchical_attributes: + # Try to find fps on parent + parent = asset['parent'] + if asset_data['visualParent'] is not None: + parent = asset_data['visualParent'] + + value = self.check_parent_hierarchical(parent, attr) + if value is None: + missing_attributes.append(attr) + else: + asset_attributes[attr] = value + + else: + missing_attributes.append(attr) + + # Raise error if attributes weren't found on asset in database + if len(missing_attributes) > 0: + atr = ", ".join(missing_attributes) + msg = 'Missing attributes "{}" in asset "{}"' + message = msg.format(atr, asset_name) + raise ValueError(message) + + # Get handles from database, Default is 0 (if not found) + handles = 0 + if "handles" in asset_data: + handles = asset_data["handles"] + + # Set frame range with handles + asset_attributes["fstart"] -= handles + asset_attributes["fend"] += handles + + # Get values from nukescript + script_attributes = { + "fps": instance_data["fps"], + "fstart": instance_data["startFrame"], + "fend": instance_data["endFrame"], + "resolution_width": instance_data["resolution_width"], + "resolution_height": instance_data["resolution_height"], + "pixel_aspect": instance_data["pixel_aspect"] + } + + # Compare asset's values Nukescript X Database + not_matching = [] + for attr in attributes: + if asset_attributes[attr] != script_attributes[attr]: + not_matching.append(attr) + + # Raise error if not matching + if len(not_matching) > 0: + msg = "Attributes '{}' aro not set correctly" + # Alert user that handles are set if Frame start/end not match + if ( + (("fstart" in not_matching) or ("fend" in not_matching)) and + (handles > 0) + ): + handles = str(handles).replace(".0", "") + msg += " (handles are set to {})".format(handles) + message = msg.format(", ".join(not_matching)) + raise ValueError(message) + + def check_parent_hierarchical(self, entityId, attr): + if entityId is None: + return None + entity = io.find_one({"_id": entityId}) + if attr in entity['data']: + return entity['data'][attr] + else: + return self.check_parent_hierarchical(entity['parent'], attr)