From a7ac81dcc10dc2b793f4acb5fa19480397621842 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 5 Dec 2018 16:58:39 +0100 Subject: [PATCH 01/13] Prepared action for event server --- pype/ftrack/events/action_sync_to_avalon.py | 366 ++++++++++++++++++++ 1 file changed, 366 insertions(+) create mode 100644 pype/ftrack/events/action_sync_to_avalon.py diff --git a/pype/ftrack/events/action_sync_to_avalon.py b/pype/ftrack/events/action_sync_to_avalon.py new file mode 100644 index 0000000000..e305b30739 --- /dev/null +++ b/pype/ftrack/events/action_sync_to_avalon.py @@ -0,0 +1,366 @@ +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 + +class Sync_To_Avalon(BaseAction): + ''' + Synchronizing data action - from Ftrack to Avalon DB + + Stores all information about entity. + - Name(string) - Most important information = identifier of entity + - Parent(ObjectId) - Avalon Project Id, if entity is not project itself + - Silo(string) - Last parent except project + - Data(dictionary): + - VisualParent(ObjectId) - Avalon Id of parent asset + - Parents(array of string) - All parent names except project + - Tasks(array of string) - Tasks on asset + - FtrackId(string) + - entityType(string) - entity's type on Ftrack + * All Custom attributes in group 'Avalon' which name don't start with 'avalon_' + + * These information are stored also for all parents and children entities. + + Avalon ID of asset is stored to Ftrack -> Custom attribute 'avalon_mongo_id'. + - action IS NOT creating this Custom attribute if doesn't exist + - run 'Create Custom Attributes' action or do it manually (Not recommended) + + If Ftrack entity already has Custom Attribute 'avalon_mongo_id' that stores ID: + - name, parents and silo are checked -> shows error if are not exact the same + - after sync it is not allowed to change names or move entities + + If ID in 'avalon_mongo_id' is empty string or is not found in DB: + - tries to find entity by name + - found: + - raise error if ftrackId/visual parent/parents are not same + - not found: + - Creates asset/project + + ''' + + #: Action identifier. + identifier = 'sync.to.avalon' + #: Action label. + label = 'SyncToAvalon' + #: 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' + + def register(self): + '''Registers the action, subscribing the the discover and launch topics.''' + self.session.event_hub.subscribe( + 'topic=ftrack.action.discover', + self._discover + ) + + self.session.event_hub.subscribe( + 'topic=ftrack.action.launch and data.actionIdentifier={0}'.format( + self.identifier + ), + self._launch + ) + + self.log.info("Action '{}' - Registered successfully".format(self.__class__.__name__)) + + def discover(self, session, entities, event): + ''' Validation ''' + roleCheck = False + discover = False + roleList = ['Administrator', 'Project Manager'] + userId = event['source']['user']['id'] + user = session.query('User where id is ' + userId).one() + + for role in user['user_security_roles']: + if role['security_role']['name'] in roleList: + roleCheck = True + if roleCheck is True: + for entity in entities: + if entity.entity_type.lower() not in ['task', 'assetversion']: + discover = True + break + + return discover + + + def launch(self, session, entities, event): + message = "" + + # JOB SETTINGS + userId = event['source']['user']['id'] + user = session.query('User where id is ' + userId).one() + + job = session.create('Job', { + 'user': user, + 'status': 'running', + 'data': json.dumps({ + 'description': 'Synch Ftrack to Avalon.' + }) + }) + + 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.importable = [] + + # get from top entity in hierarchy all parent entities + top_entity = entities[0]['link'] + if len(top_entity) > 1: + for e in top_entity: + parent_entity = session.get(e['type'], e['id']) + self.importable.append(parent_entity) + + # get all child entities separately/unique + for entity in entities: + self.getShotAsset(entity) + + # Check names: REGEX in schema/duplicates - raise error if found + all_names = [] + duplicates = [] + + for e in self.importable: + ftrack_utils.avalon_check_name(e) + if e['name'] in all_names: + duplicates.append("'{}'".format(e['name'])) + else: + all_names.append(e['name']) + + if len(duplicates) > 0: + 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() + + # Import all entities to Avalon DB + for e in self.importable: + self.importToAvalon(session, e) + + io.uninstall() + + job['status'] = 'done' + session.commit() + self.log.info('Synchronization to Avalon was successfull!') + + except ValueError as ve: + job['status'] = 'failed' + session.commit() + message = str(ve) + self.log.error('Error during syncToAvalon: {}'.format(message)) + + except Exception as e: + job['status'] = 'failed' + 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' + + if len(message) > 0: + message = "Unable to sync: {}".format(message) + return { + 'success': False, + 'message': message + } + + return { + 'success': True, + '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): + if not (entity.entity_type in ['Task']): + if entity not in self.importable: + self.importable.append(entity) + + 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() + + +def register(session, **kw): + '''Register plugin. Called when used as an plugin.''' + + # Validate that session is an instance of ftrack_api.Session. If not, + # assume that register is being called from an old or incompatible API and + # return without doing anything. + if not isinstance(session, ftrack_api.session.Session): + return + + action_handler = Sync_To_Avalon(session) + action_handler.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 2da8a47442ad381c6b503b71c9919ab290599f1b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 5 Dec 2018 19:00:29 +0100 Subject: [PATCH 02/13] preparation for 'ftrack_resources' --- pype/ftrack/actions/action_Apps.py | 10 +++++++++- pype/ftrack/actions/ftrack_action_handler.py | 17 +++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/pype/ftrack/actions/action_Apps.py b/pype/ftrack/actions/action_Apps.py index 3d1bf093de..084ffa9aec 100644 --- a/pype/ftrack/actions/action_Apps.py +++ b/pype/ftrack/actions/action_Apps.py @@ -31,11 +31,19 @@ def registerApp(app, session): label = apptoml['ftrack_label'] icon = None + ftrack_resources = "" # Path to resources here + if 'icon' in apptoml: icon = apptoml['icon'] + if '{ftrack_resources}' in icon: + icon = icon.format(ftrack_resources) + + description = None + if 'description' in apptoml: + description = apptoml['description'] # register action - AppAction(session, label, name, executable, variant, icon).register() + AppAction(session, label, name, executable, variant, icon, description).register() def register(session): diff --git a/pype/ftrack/actions/ftrack_action_handler.py b/pype/ftrack/actions/ftrack_action_handler.py index 15c57dbb1c..89fa669992 100644 --- a/pype/ftrack/actions/ftrack_action_handler.py +++ b/pype/ftrack/actions/ftrack_action_handler.py @@ -14,9 +14,6 @@ import acre from pype import api as pype -log = pype.Logger.getLogger(__name__, "ftrack") - -log.debug("pype.Anatomy: {}".format(pype.Anatomy)) class AppAction(object): @@ -231,13 +228,9 @@ class AppAction(object): entity, id = entities[0] entity = session.get(entity, id) - silo = "Film" - if entity.entity_type == "AssetBuild": - silo = "Asset" - # set environments for Avalon os.environ["AVALON_PROJECT"] = entity['project']['full_name'] - os.environ["AVALON_SILO"] = silo + 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 @@ -262,7 +255,7 @@ class AppAction(object): try: anatomy = anatomy.format(data) except Exception as e: - log.error("{0} Error in anatomy.format: {1}".format(__name__, 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) # TODO Add paths to avalon setup from tomls @@ -328,7 +321,7 @@ class AppAction(object): try: fp = open(execfile) except PermissionError as p: - log.error('Access denied on {0} - {1}'. + self.log.error('Access denied on {0} - {1}'. format(execfile, p)) return { 'success': False, @@ -338,7 +331,7 @@ class AppAction(object): fp.close() # check executable permission if not os.access(execfile, os.X_OK): - log.error('No executable permission on {}'. + self.log.error('No executable permission on {}'. format(execfile)) return { 'success': False, @@ -347,7 +340,7 @@ class AppAction(object): } pass else: - log.error('Launcher doesn\'t exist - {}'. + self.log.error('Launcher doesn\'t exist - {}'. format(execfile)) return { 'success': False, From 3fda5c32b3d9761ab3981d007b55bf53409e59cb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 6 Dec 2018 10:13:47 +0100 Subject: [PATCH 03/13] Action syncToAvalon - changed identifier and label. Is visible only for role 'Pypeclub' --- pype/ftrack/actions/action_syncToAvalon.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/ftrack/actions/action_syncToAvalon.py b/pype/ftrack/actions/action_syncToAvalon.py index cad43684c9..04e9ed53a5 100644 --- a/pype/ftrack/actions/action_syncToAvalon.py +++ b/pype/ftrack/actions/action_syncToAvalon.py @@ -48,9 +48,9 @@ class SyncToAvalon(BaseAction): ''' #: Action identifier. - identifier = 'sync.to.avalon' + identifier = 'sync.to.avalon.local' #: Action label. - label = 'SyncToAvalon' + label = 'SyncToAvalon - Local' #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. @@ -61,7 +61,7 @@ class SyncToAvalon(BaseAction): ''' Validation ''' roleCheck = False discover = False - roleList = ['Administrator', 'Project Manager'] + roleList = ['Pypeclub'] userId = event['source']['user']['id'] user = session.query('User where id is ' + userId).one() From 5eb6c48faf30122cc6ba5a44d4397c5f23bb5225 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 6 Dec 2018 11:42:48 +0100 Subject: [PATCH 04/13] Added event server class. Login dialog bugs repaired: didn't login properly through username+API; icon missing problem --- pype/ftrack/event_server.py | 36 ++++++++++++++++++++++++++++++++++++ pype/ftrack/login_dialog.py | 19 ++++++++++++++++--- 2 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 pype/ftrack/event_server.py diff --git a/pype/ftrack/event_server.py b/pype/ftrack/event_server.py new file mode 100644 index 0000000000..acc83fce0e --- /dev/null +++ b/pype/ftrack/event_server.py @@ -0,0 +1,36 @@ +import sys +import os +from pype.ftrack import credentials, login_dialog as login_dialog +from FtrackServer import FtrackServer +from app.vendor.Qt import QtCore, QtGui, QtWidgets +from pype import api + +log = api.Logger.getLogger(__name__, "ftrack-event-server") + +class EventServer: + def __init__(self): + self.login_widget = login_dialog.Login_Dialog_ui(self) + self.event_server = FtrackServer('event') + + cred = credentials._get_credentials() + + if 'username' in cred and 'apiKey' in cred: + self.login_widget.user_input.setText(cred['username']) + self.login_widget.api_input.setText(cred['apiKey']) + + self.login_widget.setError("Credentials should be for API User") + + self.login_widget.show() + + def loginChange(self): + log.info("Logged successfully") + self.login_widget.close() + self.event_server.run_server() + +def main(): + app = QtWidgets.QApplication(sys.argv) + event = EventServer() + sys.exit(app.exec_()) + +if (__name__ == ('__main__')): + main() diff --git a/pype/ftrack/login_dialog.py b/pype/ftrack/login_dialog.py index 020f917b9a..8365a6f3ab 100644 --- a/pype/ftrack/login_dialog.py +++ b/pype/ftrack/login_dialog.py @@ -23,9 +23,19 @@ class Login_Dialog_ui(QtWidgets.QWidget): self.parent = parent - self.setWindowIcon(self.parent.parent.icon) + if hasattr(parent,'icon'): + self.setWindowIcon(self.parent.icon) + elif hasattr(parent,'parent') and hasattr(parent.parent,'icon'): + self.setWindowIcon(self.parent.parent.icon) + else: + pype_setup = os.getenv('PYPE_SETUP_ROOT') + items = [pype_setup, "app", "resources", "icon.png"] + fname = os.path.sep.join(items) + icon = QtGui.QIcon(fname) + self.setWindowIcon(icon) + self.setWindowFlags(QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowMinimizeButtonHint) - + self.loginSignal.connect(self.loginWithCredentials) self._translate = QtCore.QCoreApplication.translate @@ -189,6 +199,8 @@ class Login_Dialog_ui(QtWidgets.QWidget): if verification: credentials._save_credentials(username, apiKey) credentials._set_env(username, apiKey) + if self.parent is not None: + self.parent.loginChange() self._close_widget() else: self._invalid_input(self.user_input) @@ -285,7 +297,8 @@ class Login_Dialog_ui(QtWidgets.QWidget): if verification is True: credentials._save_credentials(username, apiKey) credentials._set_env(username, apiKey) - self.parent.loginChange() + if self.parent is not None: + self.parent.loginChange() self._close_widget() def closeEvent(self, event): From 0d61f2d59f629a168a4a6a730d22f4d76b452b07 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 6 Dec 2018 11:54:04 +0100 Subject: [PATCH 05/13] Login dialog withou UI deleted --- pype/ftrack/login_dialog_noui.py | 120 ------------------------------- 1 file changed, 120 deletions(-) delete mode 100644 pype/ftrack/login_dialog_noui.py diff --git a/pype/ftrack/login_dialog_noui.py b/pype/ftrack/login_dialog_noui.py deleted file mode 100644 index 5df2c62daf..0000000000 --- a/pype/ftrack/login_dialog_noui.py +++ /dev/null @@ -1,120 +0,0 @@ -import os -import sys -import requests -import argparse -from pprint import pprint -from PyQt5 import QtCore, QtWidgets -from app import style -from . import credentials, login_tools - - -class Login_Dialog(QtWidgets.QWidget): - - loginSignal = QtCore.pyqtSignal(object, object, object) - _login_server_thread = None - - def __init__(self): - super().__init__() - self.loginSignal.connect(self.loginWithCredentials) - - def run(self): - try: - url = os.getenv('FTRACK_SERVER') - except: - print("Environment variable 'FTRACK_SERVER' is not set.") - return - - self.url = self.checkUrl(url) - self.open_ftrack() - - def open_ftrack(self): - self.loginWithCredentials(self.url, None, None) - - def checkUrl(self, url): - url = url.strip('/ ') - - if not url: - print("Url is empty!") - return - - if not 'http' in url: - if url.endswith('ftrackapp.com'): - url = 'https://' + url - else: - url = 'https://{0}.ftrackapp.com'.format(url) - try: - result = requests.get( - url, - allow_redirects=False # Old python API will not work with redirect. - ) - except requests.exceptions.RequestException: - print('The server URL set in Templates could not be reached.') - return - - - if ( - result.status_code != 200 or 'FTRACK_VERSION' not in result.headers - ): - print('The server URL set in Templates is not a valid ftrack server.') - return - - return url - - def loginWithCredentials(self, url, username, apiKey): - url = url.strip('/ ') - if not url: - print( - 'You need to specify a valid server URL, ' - 'for example https://server-name.ftrackapp.com' - ) - return - - if not 'http' in url: - if url.endswith('ftrackapp.com'): - url = 'https://' + url - else: - url = 'https://{0}.ftrackapp.com'.format(url) - try: - result = requests.get( - url, - allow_redirects=False # Old python API will not work with redirect. - ) - except requests.exceptions.RequestException: - print('The server URL you provided could not be reached.') - return - - - if ( - result.status_code != 200 or 'FTRACK_VERSION' not in result.headers - ): - print('The server URL you provided is not a valid ftrack server.') - return - - # If there is an existing server thread running we need to stop it. - if self._login_server_thread: - self._login_server_thread.quit() - self._login_server_thread = None - - # If credentials are not properly set, try to get them using a http - # server. - if not username or not apiKey: - self._login_server_thread = login_tools.LoginServerThread() - self._login_server_thread.loginSignal.connect(self.loginSignal) - self._login_server_thread.start(url) - - verification = credentials._check_credentials(username, apiKey) - - if verification is True: - credentials._save_credentials(username, apiKey) - credentials._set_env(username, apiKey) - self.close() - - -def run_login(): - app = QtWidgets.QApplication(sys.argv) - applogin = Login_Dialog() - applogin.run() - app.exec_() - -if __name__ == '__main__': - run_login() From 796388b3307c368becca76b92f32e9db57bd7311 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 6 Dec 2018 11:54:52 +0100 Subject: [PATCH 06/13] ftrackRun.py renamed to ftrack_run.py --- pype/ftrack/ftrack_run.py | 209 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 pype/ftrack/ftrack_run.py diff --git a/pype/ftrack/ftrack_run.py b/pype/ftrack/ftrack_run.py new file mode 100644 index 0000000000..7fddf171da --- /dev/null +++ b/pype/ftrack/ftrack_run.py @@ -0,0 +1,209 @@ +import sys +import os +import argparse +import subprocess +import threading +import time +from app import style +from app.vendor.Qt import QtCore, QtGui, QtWidgets +from pype.ftrack import credentials, login_dialog as login_dialog + +from FtrackServer import FtrackServer + +from pype import api as pype + + +# load data from templates +pype.load_data_from_templates() + +log = pype.Logger.getLogger(__name__, "ftrack") +# Validation if alredy logged into Ftrack + + +class FtrackRunner: + def __init__(self, main_parent=None, parent=None): + + self.parent = parent + self.loginWidget = login_dialog.Login_Dialog_ui(self) + self.actionThread = None + self.actionServer = FtrackServer('action') + self.eventThread = None + self.eventServer = FtrackServer('event') + + self.boolLogged = False + self.boolActionServer = False + self.boolEventServer = False + + def showLoginWidget(self): + self.loginWidget.show() + + def validate(self): + validation = False + cred = credentials._get_credentials() + try: + if 'username' in cred and 'apiKey' in cred: + validation = credentials._check_credentials( + cred['username'], + cred['apiKey'] + ) + if validation is False: + self.showLoginWidget() + else: + self.showLoginWidget() + + except Exception as e: + log.error("We are unable to connect to Ftrack: {0}".format(e)) + + validation = credentials._check_credentials() + if validation is True: + log.info("Connected to Ftrack successfully") + self.loginChange() + else: + log.warning("Please sign in to Ftrack") + self.boolLogged = False + self.setMenuVisibility() + + return validation + + # Necessary - login_dialog works with this method after logging in + def loginChange(self): + self.boolLogged = True + self.setMenuVisibility() + self.runActionServer() + + def logout(self): + credentials._clear_credentials() + self.stopActionServer() + self.stopEventServer() + + log.info("Logged out of Ftrack") + self.boolLogged = False + self.setMenuVisibility() + + # Actions part + def runActionServer(self): + if self.actionThread is None: + self.actionThread = threading.Thread(target=self.setActionServer) + self.actionThread.daemon = True + self.actionThread.start() + + log.info("Ftrack action server launched") + self.boolActionServer = True + self.setMenuVisibility() + + def setActionServer(self): + self.actionServer.run_server() + + def resetActionServer(self): + self.stopActionServer() + self.runActionServer() + + def stopActionServer(self): + try: + self.actionServer.stop_session() + if self.actionThread is not None: + self.actionThread.join() + self.actionThread = None + + log.info("Ftrack action server stopped") + self.boolActionServer = False + self.setMenuVisibility() + except Exception as e: + log.error("During Killing action server: {0}".format(e)) + + # Events part + def runEventServer(self): + if self.eventThread is None: + self.eventThread = threading.Thread(target=self.setEventServer) + self.eventThread.daemon = True + self.eventThread.start() + + log.info("Ftrack event server launched") + self.boolEventServer = True + self.setMenuVisibility() + + def setEventServer(self): + self.eventServer.run_server() + + def resetEventServer(self): + self.stopEventServer() + self.runEventServer() + + def stopEventServer(self): + try: + self.eventServer.stop_session() + if self.eventThread is not None: + self.eventThread.join() + self.eventThread = None + + log.info("Ftrack event server stopped") + self.boolEventServer = False + self.setMenuVisibility() + except Exception as e: + log.error("During Killing Event server: {0}".format(e)) + + # Definition of Tray menu + def trayMenu(self, parent): + # Menu for Tray App + self.menu = QtWidgets.QMenu('Ftrack', parent) + self.menu.setProperty('submenu', 'on') + self.menu.setStyleSheet(style.load_stylesheet()) + + # Actions - server + self.smActionS = self.menu.addMenu("Action server") + self.aRunActionS = QtWidgets.QAction("Run action server", self.smActionS) + self.aRunActionS.triggered.connect(self.runActionServer) + self.aResetActionS = QtWidgets.QAction("Reset action server", self.smActionS) + self.aResetActionS.triggered.connect(self.resetActionServer) + self.aStopActionS = QtWidgets.QAction("Stop action server", self.smActionS) + self.aStopActionS.triggered.connect(self.stopActionServer) + + self.smActionS.addAction(self.aRunActionS) + self.smActionS.addAction(self.aResetActionS) + self.smActionS.addAction(self.aStopActionS) + + # Actions - server + self.smEventS = self.menu.addMenu("Event server") + self.aRunEventS = QtWidgets.QAction("Run event server", self.smEventS) + self.aRunEventS.triggered.connect(self.runEventServer) + self.aResetEventS = QtWidgets.QAction("Reset event server", self.smEventS) + self.aResetEventS.triggered.connect(self.resetEventServer) + self.aStopEventS = QtWidgets.QAction("Stop event server", self.smEventS) + self.aStopEventS.triggered.connect(self.stopEventServer) + + self.smEventS.addAction(self.aRunEventS) + self.smEventS.addAction(self.aResetEventS) + self.smEventS.addAction(self.aStopEventS) + + # Actions - basic + self.aLogin = QtWidgets.QAction("Login", self.menu) + self.aLogin.triggered.connect(self.validate) + self.aLogout = QtWidgets.QAction("Logout", self.menu) + self.aLogout.triggered.connect(self.logout) + + self.menu.addAction(self.aLogin) + self.menu.addAction(self.aLogout) + + self.boolLogged = False + self.setMenuVisibility() + + return self.menu + + # Definition of visibility of each menu actions + def setMenuVisibility(self): + + self.smActionS.menuAction().setVisible(self.boolLogged) + self.smEventS.menuAction().setVisible(self.boolLogged) + self.aLogin.setVisible(not self.boolLogged) + self.aLogout.setVisible(self.boolLogged) + + if self.boolLogged is False: + return + + self.aRunActionS.setVisible(not self.boolActionServer) + self.aResetActionS.setVisible(self.boolActionServer) + self.aStopActionS.setVisible(self.boolActionServer) + + self.aRunEventS.setVisible(not self.boolEventServer) + self.aResetEventS.setVisible(self.boolEventServer) + self.aStopEventS.setVisible(self.boolEventServer) From 64bebac3944eb759016067a2ebdb0893f49fa28c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 6 Dec 2018 12:49:43 +0100 Subject: [PATCH 07/13] Removed event server from Tray --- pype/ftrack/ftrack_run.py | 52 --------------------------------------- 1 file changed, 52 deletions(-) diff --git a/pype/ftrack/ftrack_run.py b/pype/ftrack/ftrack_run.py index 7fddf171da..fcf1f621fe 100644 --- a/pype/ftrack/ftrack_run.py +++ b/pype/ftrack/ftrack_run.py @@ -27,12 +27,9 @@ class FtrackRunner: self.loginWidget = login_dialog.Login_Dialog_ui(self) self.actionThread = None self.actionServer = FtrackServer('action') - self.eventThread = None - self.eventServer = FtrackServer('event') self.boolLogged = False self.boolActionServer = False - self.boolEventServer = False def showLoginWidget(self): self.loginWidget.show() @@ -74,7 +71,6 @@ class FtrackRunner: def logout(self): credentials._clear_credentials() self.stopActionServer() - self.stopEventServer() log.info("Logged out of Ftrack") self.boolLogged = False @@ -111,36 +107,6 @@ class FtrackRunner: except Exception as e: log.error("During Killing action server: {0}".format(e)) - # Events part - def runEventServer(self): - if self.eventThread is None: - self.eventThread = threading.Thread(target=self.setEventServer) - self.eventThread.daemon = True - self.eventThread.start() - - log.info("Ftrack event server launched") - self.boolEventServer = True - self.setMenuVisibility() - - def setEventServer(self): - self.eventServer.run_server() - - def resetEventServer(self): - self.stopEventServer() - self.runEventServer() - - def stopEventServer(self): - try: - self.eventServer.stop_session() - if self.eventThread is not None: - self.eventThread.join() - self.eventThread = None - - log.info("Ftrack event server stopped") - self.boolEventServer = False - self.setMenuVisibility() - except Exception as e: - log.error("During Killing Event server: {0}".format(e)) # Definition of Tray menu def trayMenu(self, parent): @@ -162,19 +128,6 @@ class FtrackRunner: self.smActionS.addAction(self.aResetActionS) self.smActionS.addAction(self.aStopActionS) - # Actions - server - self.smEventS = self.menu.addMenu("Event server") - self.aRunEventS = QtWidgets.QAction("Run event server", self.smEventS) - self.aRunEventS.triggered.connect(self.runEventServer) - self.aResetEventS = QtWidgets.QAction("Reset event server", self.smEventS) - self.aResetEventS.triggered.connect(self.resetEventServer) - self.aStopEventS = QtWidgets.QAction("Stop event server", self.smEventS) - self.aStopEventS.triggered.connect(self.stopEventServer) - - self.smEventS.addAction(self.aRunEventS) - self.smEventS.addAction(self.aResetEventS) - self.smEventS.addAction(self.aStopEventS) - # Actions - basic self.aLogin = QtWidgets.QAction("Login", self.menu) self.aLogin.triggered.connect(self.validate) @@ -193,7 +146,6 @@ class FtrackRunner: def setMenuVisibility(self): self.smActionS.menuAction().setVisible(self.boolLogged) - self.smEventS.menuAction().setVisible(self.boolLogged) self.aLogin.setVisible(not self.boolLogged) self.aLogout.setVisible(self.boolLogged) @@ -203,7 +155,3 @@ class FtrackRunner: self.aRunActionS.setVisible(not self.boolActionServer) self.aResetActionS.setVisible(self.boolActionServer) self.aStopActionS.setVisible(self.boolActionServer) - - self.aRunEventS.setVisible(not self.boolEventServer) - self.aResetEventS.setVisible(self.boolEventServer) - self.aStopEventS.setVisible(self.boolEventServer) From ea7a10f94f7f2b4c65074b3edaefeee9e9b105d9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 6 Dec 2018 12:52:58 +0100 Subject: [PATCH 08/13] renamed file --- .../{action_syncToAvalon.py => action_sync_to_avalon_local.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pype/ftrack/actions/{action_syncToAvalon.py => action_sync_to_avalon_local.py} (100%) diff --git a/pype/ftrack/actions/action_syncToAvalon.py b/pype/ftrack/actions/action_sync_to_avalon_local.py similarity index 100% rename from pype/ftrack/actions/action_syncToAvalon.py rename to pype/ftrack/actions/action_sync_to_avalon_local.py From 5904fa4c65820a06121d46598fc02f29ca8377ea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 6 Dec 2018 12:55:05 +0100 Subject: [PATCH 09/13] Revert "renamed file" This reverts commit ea7a10f94f7f2b4c65074b3edaefeee9e9b105d9. --- .../{action_sync_to_avalon_local.py => action_syncToAvalon.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pype/ftrack/actions/{action_sync_to_avalon_local.py => action_syncToAvalon.py} (100%) diff --git a/pype/ftrack/actions/action_sync_to_avalon_local.py b/pype/ftrack/actions/action_syncToAvalon.py similarity index 100% rename from pype/ftrack/actions/action_sync_to_avalon_local.py rename to pype/ftrack/actions/action_syncToAvalon.py From f59bcf2e44eec0708649b4fdd9ce45e517829055 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 6 Dec 2018 12:57:44 +0100 Subject: [PATCH 10/13] renamed filename --- .../{action_syncToAvalon.py => action_sync_to_avalon_local.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pype/ftrack/actions/{action_syncToAvalon.py => action_sync_to_avalon_local.py} (100%) diff --git a/pype/ftrack/actions/action_syncToAvalon.py b/pype/ftrack/actions/action_sync_to_avalon_local.py similarity index 100% rename from pype/ftrack/actions/action_syncToAvalon.py rename to pype/ftrack/actions/action_sync_to_avalon_local.py From e19fbea3e9fb18c243a05ce034308e33eceb75ad Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 6 Dec 2018 15:20:05 +0100 Subject: [PATCH 11/13] Identifier is now full app name(with version), so same app won't launch twice if two versions are available. --- pype/ftrack/actions/action_Apps.py | 3 ++- pype/ftrack/actions/ftrack_action_handler.py | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pype/ftrack/actions/action_Apps.py b/pype/ftrack/actions/action_Apps.py index 3d1bf093de..76c6ba1e06 100644 --- a/pype/ftrack/actions/action_Apps.py +++ b/pype/ftrack/actions/action_Apps.py @@ -9,7 +9,7 @@ from app.api import Logger log = Logger.getLogger(__name__) def registerApp(app, session): - name = app['name'].split("_")[0] + name = app['name'].replace("_", ".") variant = "" try: variant = app['name'].split("_")[1] @@ -59,6 +59,7 @@ def register(session): appNames.append(app['name']) apps.append(app) + apps = sorted(apps, key=lambda x: x['name']) for app in apps: try: registerApp(app, session) diff --git a/pype/ftrack/actions/ftrack_action_handler.py b/pype/ftrack/actions/ftrack_action_handler.py index 15c57dbb1c..63561951d4 100644 --- a/pype/ftrack/actions/ftrack_action_handler.py +++ b/pype/ftrack/actions/ftrack_action_handler.py @@ -72,9 +72,7 @@ class AppAction(object): ), self._launch ) - self.log.info("Application '{}' - Registered successfully".format(self.label)) - - self.log.info("Application '{}' - Registered successfully".format(self.label)) + self.log.info("Application '{} {}' - Registered successfully".format(self.label,self.variant)) def _discover(self, event): args = self._translate_event( From 3b3a379b7a722a8438af091cbaf63e9832cd3a09 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 7 Dec 2018 01:12:34 +0100 Subject: [PATCH 12/13] fixing issue with apps not launchign at all after last merge --- pype/ftrack/actions/action_Apps.py | 3 ++- pype/ftrack/actions/ftrack_action_handler.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pype/ftrack/actions/action_Apps.py b/pype/ftrack/actions/action_Apps.py index 76c6ba1e06..23667290bd 100644 --- a/pype/ftrack/actions/action_Apps.py +++ b/pype/ftrack/actions/action_Apps.py @@ -9,7 +9,7 @@ from app.api import Logger log = Logger.getLogger(__name__) def registerApp(app, session): - name = app['name'].replace("_", ".") + name = app['name'] variant = "" try: variant = app['name'].split("_")[1] @@ -17,6 +17,7 @@ def registerApp(app, session): log.warning("'{0}' - App 'name' and 'variant' is not separated by '_' (variant is not set)".format(app['name'])) return + log.warning("app name {}".format(name)) abspath = lib.which_app(app['name']) if abspath == None: log.error("'{0}' - App don't have config toml file".format(app['name'])) diff --git a/pype/ftrack/actions/ftrack_action_handler.py b/pype/ftrack/actions/ftrack_action_handler.py index 63561951d4..fb49c40464 100644 --- a/pype/ftrack/actions/ftrack_action_handler.py +++ b/pype/ftrack/actions/ftrack_action_handler.py @@ -135,7 +135,7 @@ class AppAction(object): else: apps = [] for app in project['config']['apps']: - apps.append(app['name'].split("_")[0]) + apps.append(app['name']) if self.identifier not in apps: return False @@ -238,8 +238,8 @@ class AppAction(object): os.environ["AVALON_SILO"] = silo os.environ["AVALON_ASSET"] = entity['parent']['name'] os.environ["AVALON_TASK"] = entity['name'] - os.environ["AVALON_APP"] = self.identifier - os.environ["AVALON_APP_NAME"] = self.identifier + "_" + self.variant + os.environ["AVALON_APP"] = self.identifier.split("_")[0] + os.environ["AVALON_APP_NAME"] = self.identifier os.environ["FTRACK_TASKID"] = id @@ -280,7 +280,7 @@ class AppAction(object): parents.append(session.get(item['type'], item['id'])) # collect all the 'environment' attributes from parents - tools_attr = [os.environ["AVALON_APP_NAME"]] + tools_attr = [os.environ["AVALON_APP"], os.environ["AVALON_APP_NAME"]] for parent in reversed(parents): # check if the attribute is empty, if not use it if parent['custom_attributes']['tools_env']: From 3a38a907847f126df819da80f7e9b00ad3e23a8d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 7 Dec 2018 09:24:07 +0100 Subject: [PATCH 13/13] ftrackRun renamed to ftrack_run --- pype/ftrack/{ftrackRun.py => ftrack_run.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pype/ftrack/{ftrackRun.py => ftrack_run.py} (100%) diff --git a/pype/ftrack/ftrackRun.py b/pype/ftrack/ftrack_run.py similarity index 100% rename from pype/ftrack/ftrackRun.py rename to pype/ftrack/ftrack_run.py