From 129ac28b2189f180f3e38676d9ce83419bf5b06d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Oct 2018 09:45:06 +0200 Subject: [PATCH] added Task import to syncToAvalon, preparing apps actions --- pype/ftrack/__init__.py | 22 -- pype/ftrack/actions/action_Apps.py | 32 +++ pype/ftrack/actions/action_killRunningJobs.py | 28 +- pype/ftrack/actions/action_sTa_opt.py | 154 +++++++++++ pype/ftrack/actions/action_syncToAvalon.py | 38 +-- pype/ftrack/actions/action_syncToAvalon_v2.py | 247 ++++++++++++++++++ .../actions/{test.py => action_test.py} | 21 +- .../actions/connect_discoverApplications.py | 169 ++++++++++++ pype/ftrack/create_custAttributes_AvalonId.py | 51 ++++ pype/ftrack/events/file_version_statuses.py | 38 +++ pype/ftrack/events/new_task_update.py | 81 ++++++ pype/ftrack/events/radio_buttons.py | 39 +++ pype/ftrack/events/test_event.py | 28 ++ pype/ftrack/events/thumbnail_updates.py | 43 +++ pype/ftrack/events/version_to_task_status.py | 62 +++++ pype/ftrack/ftrack_utils.py | 43 +-- pype/ftrack/test_events/test_event.py | 16 ++ pype/ftrack/test_events/test_event2.py | 16 ++ pype/vendor/ftrack_action_handler/action.py | 3 +- .../vendor/ftrack_action_handler/appaction.py | 240 +++++++++++++++++ 20 files changed, 1251 insertions(+), 120 deletions(-) create mode 100644 pype/ftrack/actions/action_Apps.py create mode 100644 pype/ftrack/actions/action_sTa_opt.py create mode 100644 pype/ftrack/actions/action_syncToAvalon_v2.py rename pype/ftrack/actions/{test.py => action_test.py} (77%) create mode 100644 pype/ftrack/actions/connect_discoverApplications.py create mode 100644 pype/ftrack/create_custAttributes_AvalonId.py create mode 100644 pype/ftrack/events/file_version_statuses.py create mode 100644 pype/ftrack/events/new_task_update.py create mode 100644 pype/ftrack/events/radio_buttons.py create mode 100644 pype/ftrack/events/test_event.py create mode 100644 pype/ftrack/events/thumbnail_updates.py create mode 100644 pype/ftrack/events/version_to_task_status.py create mode 100644 pype/ftrack/test_events/test_event.py create mode 100644 pype/ftrack/test_events/test_event2.py create mode 100644 pype/vendor/ftrack_action_handler/appaction.py diff --git a/pype/ftrack/__init__.py b/pype/ftrack/__init__.py index 7ef7d45ec3..e69de29bb2 100644 --- a/pype/ftrack/__init__.py +++ b/pype/ftrack/__init__.py @@ -1,22 +0,0 @@ -# Module Arrow,clique need to be installed!!!!!!!!! -import ftrack_api - -# TO DO load config with column name that need to be imported/exported - - -import os - -from avalon import api as avalon -from pyblish import api as pyblish -from avalon import io - - -#project = Avalonsession.find({"type": "project"}) - -# Action - On create/change/delete event chekc if data are send data to avalon - -session = ftrack_api.Session( - server_url="https://pype.ftrackapp.com", - api_key="4e01eda0-24b3-4451-8e01-70edc03286be", - api_user="jakub.trllo" -) diff --git a/pype/ftrack/actions/action_Apps.py b/pype/ftrack/actions/action_Apps.py new file mode 100644 index 0000000000..d869a764a6 --- /dev/null +++ b/pype/ftrack/actions/action_Apps.py @@ -0,0 +1,32 @@ +import os +import logging +import ftrack_api +from ftrack_action_handler.appaction import AppAction +from avalon import io + + +os.environ['AVALON_PROJECTS']='tmp' +io.install() +projects = sorted(io.projects(), key=lambda x: x['name']) +io.uninstall() + +# Temporary +s = ftrack_api.Session( + server_url="https://pype.ftrackapp.com", + api_key="4e01eda0-24b3-4451-8e01-70edc03286be", + api_user="jakub.trllo" +) + +def register(session): + actions = [] + for project in projects: + os.environ['AVALON_PROJECTS'] = project['name'] + for app in project['config']['apps']: + actions.append(AppAction(session, project['name'], app['label'], app['name'])) + for action in actions: + action.register() + print(actions) + + session.event_hub.wait() + +register(s) diff --git a/pype/ftrack/actions/action_killRunningJobs.py b/pype/ftrack/actions/action_killRunningJobs.py index 585c4c02ba..44058a02d6 100644 --- a/pype/ftrack/actions/action_killRunningJobs.py +++ b/pype/ftrack/actions/action_killRunningJobs.py @@ -3,43 +3,26 @@ import sys import argparse import logging -import collections -import os + import datetime -import json import ftrack_api from ftrack_action_handler.action import BaseAction -from avalon import io, inventory, schema -from avalon.vendor import toml - class JobKiller(BaseAction): '''Edit meta data action.''' #: Action identifier. identifier = 'job.kill' - #: Action label. label = 'Job Killer' - #: Action description. description = 'Killing all running jobs younger than day' + def validate_selection(self, session, entities): '''Return if *entities* is a valid selection.''' - # if (len(entities) != 1): - # # If entities contains more than one item return early since - # # metadata cannot be edited for several entites at the same time. - # return False pass - # entity_type, entity_id = entities[0] - # if ( - # entity_type not in session.types - # ): - # # Return False if the target entity does not have a metadata - # # attribute. - # return False return True @@ -67,8 +50,11 @@ class JobKiller(BaseAction): session.commit() - print('Complete') - return True + print('All running jobs were killed Successfully!') + return { + 'success': True, + 'message': 'All running jobs were killed Successfully!' + } def register(session, **kw): diff --git a/pype/ftrack/actions/action_sTa_opt.py b/pype/ftrack/actions/action_sTa_opt.py new file mode 100644 index 0000000000..1fb75a03b5 --- /dev/null +++ b/pype/ftrack/actions/action_sTa_opt.py @@ -0,0 +1,154 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2017 ftrack +import sys +import argparse +import logging +import collections +import os +import json + +import ftrack_api +from ftrack_action_handler.action import BaseAction +from avalon import io, inventory, schema +from avalon.vendor import toml + + +class TestAction(BaseAction): + '''Edit meta data action.''' + + #: Action identifier. + identifier = 'test.action' + + #: Action label. + label = 'Test action' + + #: Action description. + description = 'Test action' + + def validate_selection(self, session, entities): + '''Return if *entities* is a valid selection.''' + pass + return True + + def discover(self, session, entities, event): + '''Return True if action is valid.''' + + self.logger.info('Got selection: {0}'.format(entities)) + return self.validate_selection(session, entities) + + def launch(self, session, entities, event): + + def fromTop(index, max, inEnt): + output=dict() + entity = session.get(inEnt[index]['type'], inEnt[index]['id']) + tasks=[] + for e in entity['children']: + if e.entity_type in ['Task']: + tasks.append(e['name']) + # Get info about all parents + if index < max: + output = {'name': entity['name'], 'tasks': tasks, 'childrens': fromTop(index+1, max, inEnt)} + # Get info about all childrens + else: + childrens = [] + for e in entity['children']: + if e.entity_type not in ['Task']: + childrens.append(toBottom(e)) + output = {'name': entity['name'], 'tasks': tasks, 'childrens': childrens} + return output + + def toBottom(entity): + tasks = [] + childrens = [] + # If entity have childrens do: + if entity['children']: + # Get all tasks + for e in entity['children']: + if e.entity_type in ['Task']: + tasks.append(e['name']) + # Get childrens of children + for e in entity['children']: + if e.entity_type not in ['Task']: + childrens.append(toBottom(e)) + return {'name': entity['name'], 'tasks': tasks, 'childrens': childrens} + + def project(proj): + type = 'project' + addProjectToDatabase() + projId = getIdFromDb() + if len(proj['childrens'] > 0): + childrens = proj['childrens'] + test(childrens, None, projId) + + def test(childrens, parentId, projId): + type = 'asset' + for child in childrens: + silo = 'Assets' if child['type'] in ['AssetBuild', 'Library'] else 'Film' + addtodatabase() + newId = fromdatabase() + if len(child['childrens']) > 0: + test(child, newId, projId) + + + for entity in entities: + entity_type, entity_id = entity + entity = session.get(entity_type, entity_id) + max = len(entity['link']) - 1 + out = fromTop(0, max, entity['link']) + print(100*"_") + print(out) + + return True + + +def register(session, **kw): + '''Register plugin. Called when used as an plugin.''' + + if not isinstance(session, ftrack_api.session.Session): + return + + action_handler = TestAction(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( + server_url="https://pype.ftrackapp.com", + api_key="4e01eda0-24b3-4451-8e01-70edc03286be", + api_user="jakub.trllo" + ) + 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:])) diff --git a/pype/ftrack/actions/action_syncToAvalon.py b/pype/ftrack/actions/action_syncToAvalon.py index 0440d09f69..81c88ab651 100644 --- a/pype/ftrack/actions/action_syncToAvalon.py +++ b/pype/ftrack/actions/action_syncToAvalon.py @@ -3,9 +3,7 @@ import sys import argparse import logging -import collections import os - import json import ftrack_api from ftrack_action_handler.action import BaseAction @@ -32,7 +30,6 @@ class SyncToAvalon(BaseAction): # # If entities contains more than one item return early since # # metadata cannot be edited for several entites at the same time. # return False - pass # entity_type, entity_id = entities[0] # if ( # entity_type not in session.types @@ -40,7 +37,7 @@ class SyncToAvalon(BaseAction): # # Return False if the target entity does not have a metadata # # attribute. # return False - + pass return True def discover(self, session, entities, event): @@ -49,6 +46,7 @@ class SyncToAvalon(BaseAction): self.logger.info('Got selection: {0}'.format(entities)) return self.validate_selection(session, entities) + def importToAvalon(self, session, entity): eLinks = [] custAttrName = 'avalon_mongo_id' @@ -60,7 +58,7 @@ class SyncToAvalon(BaseAction): name = e['name'] else: name = e['name'].replace(" ", "-") - print("Name of "+tmp.entity_type+" was changed from "+e['name']+" to "+name) + print("Name of "+tmp.entity_type+" - "+e['name']+" was changed to "+name) eLinks.append({"type": tmp.entity_type, "name": name, "ftrackId": tmp['id']}) @@ -81,7 +79,7 @@ class SyncToAvalon(BaseAction): except IOError: raise - # Create project in Avalon + # --- Create project and assets in Avalon --- io.install() # Check if project exists -> Create project if (io.find_one( @@ -93,30 +91,32 @@ class SyncToAvalon(BaseAction): if custAttrName in entityProj['custom_attributes'] and entityProj['custom_attributes'][custAttrName] is '': entityProj['custom_attributes'][custAttrName] = str(projectId) - # If entity is Project or Silo kill action - if (len(eLinks) > 2) and not (eLinks[-1]['type'] in ['Project']): - silo = eLinks[1] + # If entity is Project or have only 1 entity kill action + if (len(eLinks) > 1) and not (eLinks[-1]['type'] in ['Project']): + # TODO how to check if entity is Asset Library or AssetBuild? + silo = 'Assets' if eLinks[-1]['type'] in ['AssetBuild', 'Library'] else 'Film' + os.environ['AVALON_SILO'] = silo # Create Assets assets = [] - for i in range(2, len(eLinks)): + for i in range(1, len(eLinks)): assets.append(eLinks[i]) folderStruct = [] - folderStruct.append(silo['name']) parentId = None data = {'visualParent': parentId, 'parents': folderStruct, 'ftrackId': None, 'entityType': None} for asset in assets: + + os.environ['AVALON_ASSET'] = asset['name'] data.update({'ftrackId': asset['ftrackId'], 'entityType': asset['type']}) if (io.find_one({'type': 'asset', 'name': asset['name']}) is None): - inventory.create_asset(asset['name'], silo['name'], data, projectId) + inventory.create_asset(asset['name'], silo, data, projectId) print("Asset "+asset['name']+" created") else: # TODO check if is asset in same folder!!! ???? FEATURE FOR FUTURE - # tmp = io.find_one({'type': 'asset', 'name': asset['name']}) print("Asset "+asset["name"]+" already exist") parentId = io.find_one({'type': 'asset', 'name': asset['name']})['_id'] @@ -144,6 +144,10 @@ class SyncToAvalon(BaseAction): }) try: + #TODO It's better to have these env set, are they used anywhere? + os.environ['AVALON_PROJECTS'] = "tmp" + os.environ['AVALON_ASSET'] = "tmp" + os.environ['AVALON_SILO'] = "tmp" importable = [] def getShotAsset(entity): @@ -167,8 +171,13 @@ class SyncToAvalon(BaseAction): job['status'] = 'done' session.commit() - except: + + print('Synchronization to Avalon was successfull!') + except(e): job['status'] = 'failed' + print('During synchronization to Avalon went something wrong!') + print(e) + return True @@ -213,7 +222,6 @@ def main(arguments=None): session = ftrack_api.Session( server_url="https://pype.ftrackapp.com", api_key="4e01eda0-24b3-4451-8e01-70edc03286be", - api_user="jakub.trllo" ) register(session) diff --git a/pype/ftrack/actions/action_syncToAvalon_v2.py b/pype/ftrack/actions/action_syncToAvalon_v2.py new file mode 100644 index 0000000000..0360223f9c --- /dev/null +++ b/pype/ftrack/actions/action_syncToAvalon_v2.py @@ -0,0 +1,247 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2017 ftrack +import sys +import argparse +import logging +import os +import json +import ftrack_api +from ftrack_action_handler.action import BaseAction + +from avalon import io, inventory, schema +from avalon.vendor import toml + + +class SyncToAvalon(BaseAction): + '''Edit meta data action.''' + + #: 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 validate_selection(self, session, entities): + '''Return if *entities* is a valid selection.''' + # if (len(entities) != 1): + # # If entities contains more than one item return early since + # # metadata cannot be edited for several entites at the same time. + # return False + # entity_type, entity_id = entities[0] + # if ( + # entity_type not in session.types + # ): + # # Return False if the target entity does not have a metadata + # # attribute. + # return False + pass + return True + + def discover(self, session, entities, event): + '''Return True if action is valid.''' + + self.logger.info('Got selection: {0}'.format(entities)) + return self.validate_selection(session, entities) + + + def importToAvalon(self, session, entity): + eLinks = [] + custAttrName = 'avalon_mongo_id' + # TODO read from file, which data are in scope??? + # get needed info of entity and all parents + for e in entity['link']: + tmp = session.get(e['type'], e['id']) + if e['name'].find(" ") == -1: + name = e['name'] + else: + name = e['name'].replace(" ", "-") + print("Name of "+tmp.entity_type+" - "+e['name']+" was changed to "+name) + + eLinks.append({"type": tmp.entity_type, "name": name, "ftrackId": tmp['id']}) + + entityProj = session.get(eLinks[0]['type'], eLinks[0]['ftrackId']) + + # set AVALON_PROJECT env + os.environ["AVALON_PROJECT"] = entityProj["full_name"] + + # get schema of project TODO read different schemas based on project type + template = {"schema": "avalon-core:inventory-1.0"} + schema = entityProj['project_schema']['name'] + fname = os.path.join(os.path.dirname( + os.path.realpath(__file__)), + (schema + '.toml')) + try: + with open(fname) as f: + config = toml.load(f) + except IOError: + raise + + # --- Create project and assets in Avalon --- + io.install() + # Check if project exists -> Create project + if (io.find_one( + {"type": "project", "name": entityProj["full_name"]}) is None): + inventory.save(entityProj["full_name"], config, template) + + # Store project Id + projectId = io.find_one({"type": "project", "name": entityProj["full_name"]})["_id"] + if custAttrName in entityProj['custom_attributes'] and entityProj['custom_attributes'][custAttrName] is '': + entityProj['custom_attributes'][custAttrName] = str(projectId) + + # If entity is Project or have only 1 entity kill action + if (len(eLinks) > 1) and not (eLinks[-1]['type'] in ['Project']): + + # TODO how to check if entity is Asset Library or AssetBuild? + silo = 'Assets' if eLinks[-1]['type'] in ['AssetBuild', 'Library'] else 'Film' + os.environ['AVALON_SILO'] = silo + # Create Assets + assets = [] + for i in range(1, len(eLinks)): + assets.append(eLinks[i]) + + folderStruct = [] + parentId = None + data = {'visualParent': parentId, 'parents': folderStruct, + 'tasks':None, 'ftrackId': None, 'entityType': None} + + for asset in assets: + os.environ['AVALON_ASSET'] = asset['name'] + data.update({'ftrackId': asset['ftrackId'], 'entityType': asset['type']}) + # Get tasks of each asset + assetEnt = session.get('TypedContext', asset['ftrackId']) + tasks = [] + for child in assetEnt['children']: + if child.entity_type in ['Task']: + tasks.append(child['name']) + data.update({'tasks': tasks}) + + if (io.find_one({'type': 'asset', 'name': asset['name']}) is None): + # Create asset in DB + inventory.create_asset(asset['name'], silo, data, projectId) + print("Asset "+asset['name']+" - created") + else: + io.update_many({'type': 'asset','name': asset['name']}, + {'$set':{'data':data}}) + # TODO check if is asset in same folder!!! ???? FEATURE FOR FUTURE + print("Asset "+asset["name"]+" - already exist") + + parentId = io.find_one({'type': 'asset', 'name': asset['name']})['_id'] + data.update({'visualParent': parentId, 'parents': folderStruct}) + folderStruct.append(asset['name']) + + + # Set custom attribute to avalon/mongo id of entity (parentID is last) + if custAttrName in entity['custom_attributes'] and entity['custom_attributes'][custAttrName] is '': + entity['custom_attributes'][custAttrName] = str(parentId) + + io.uninstall() + + def launch(self, session, entities, event): + # 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: + print("action <" + self.__class__.__name__ + "> is running") + #TODO It's better to have these env set, are they used anywhere? + os.environ['AVALON_PROJECTS'] = "tmp" + os.environ['AVALON_ASSET'] = "tmp" + os.environ['AVALON_SILO'] = "tmp" + importable = [] + + def getShotAsset(entity): + if not (entity.entity_type in ['Task']): + if entity not in importable: + importable.append(entity) + + if entity['children']: + childrens = entity['children'] + for child in childrens: + getShotAsset(child) + + # get all entities separately + for entity in entities: + entity_type, entity_id = entity + act_ent = session.get(entity_type, entity_id) + getShotAsset(act_ent) + + for e in importable: + self.importToAvalon(session, e) + + job['status'] = 'done' + session.commit() + + print('Synchronization to Avalon was successfull!') + except Exception as e: + job['status'] = 'failed' + print('During synchronization to Avalon went something wrong!') + print(e) + + return True + + +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 = SyncToAvalon(session) + action_handler.register() + print("----- action - <" + action_handler.__class__.__name__ + "> - Has been registered -----") + + +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( + server_url="https://pype.ftrackapp.com", + api_key="4e01eda0-24b3-4451-8e01-70edc03286be", + ) + 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:])) diff --git a/pype/ftrack/actions/test.py b/pype/ftrack/actions/action_test.py similarity index 77% rename from pype/ftrack/actions/test.py rename to pype/ftrack/actions/action_test.py index c9db886006..8cab6924da 100644 --- a/pype/ftrack/actions/test.py +++ b/pype/ftrack/actions/action_test.py @@ -5,11 +5,10 @@ import argparse import logging import collections import os - import json + import ftrack_api from ftrack_action_handler.action import BaseAction - from avalon import io, inventory, schema from avalon.vendor import toml @@ -28,19 +27,7 @@ class TestAction(BaseAction): def validate_selection(self, session, entities): '''Return if *entities* is a valid selection.''' - # if (len(entities) != 1): - # # If entities contains more than one item return early since - # # metadata cannot be edited for several entites at the same time. - # return False pass - # entity_type, entity_id = entities[0] - # if ( - # entity_type not in session.types - # ): - # # Return False if the target entity does not have a metadata - # # attribute. - # return False - return True def discover(self, session, entities, event): @@ -50,17 +37,13 @@ class TestAction(BaseAction): return self.validate_selection(session, entities) def launch(self, session, entities, event): - """ JOB SETTING """ - + return True 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 diff --git a/pype/ftrack/actions/connect_discoverApplications.py b/pype/ftrack/actions/connect_discoverApplications.py new file mode 100644 index 0000000000..190467ef5e --- /dev/null +++ b/pype/ftrack/actions/connect_discoverApplications.py @@ -0,0 +1,169 @@ +def _discoverApplications(self): + '''Return a list of applications that can be launched from this host. + + An application should be of the form: + + dict( + 'identifier': 'name_version', + 'label': 'Name', + 'variant': 'version', + 'description': 'description', + 'path': 'Absolute path to the file', + 'version': 'Version of the application', + 'icon': 'URL or name of predefined icon' + ) + + ''' + applications = [] + + if sys.platform == 'darwin': + prefix = ['/', 'Applications'] + + elif sys.platform == 'win32': + prefix = ['C:\\', 'Program Files.*'] + + self.logger.debug( + 'Discovered applications:\n{0}'.format( + pprint.pformat(applications) + ) + ) + + return applications + +class ApplicationLauncher(object): + '''Launch applications described by an application store. + + Launched applications are started detached so exiting current process will + not close launched applications. + + ''' + + def __init__(self, applicationStore): + '''Instantiate launcher with *applicationStore* of applications. + + *applicationStore* should be an instance of :class:`ApplicationStore` + holding information about applications that can be launched. + + ''' + super(ApplicationLauncher, self).__init__() + self.logger = logging.getLogger( + __name__ + '.' + self.__class__.__name__ + ) + + self.applicationStore = applicationStore + + def launch(self, applicationIdentifier, context=None): + '''Launch application matching *applicationIdentifier*. + + *context* should provide information that can guide how to launch the + application. + + Return a dictionary of information containing: + + success - A boolean value indicating whether application launched + successfully or not. + message - Any additional information (such as a failure message). + + ''' + # Look up application. + applicationIdentifierPattern = applicationIdentifier + if applicationIdentifierPattern == 'hieroplayer': + applicationIdentifierPattern += '*' + + application = self.applicationStore.getApplication( + applicationIdentifierPattern + ) + + if application is None: + return { + 'success': False, + 'message': ( + '{0} application not found.' + .format(applicationIdentifier) + ) + } + + # Construct command and environment. + command = self._getApplicationLaunchCommand(application, context) + environment = self._getApplicationEnvironment(application, context) + + # Environment must contain only strings. + self._conformEnvironment(environment) + + success = True + message = '{0} application started.'.format(application['label']) + + try: + options = dict( + env=environment, + close_fds=True + ) + + # Ensure that current working directory is set to the root of the + # application being launched to avoid issues with applications + # locating shared libraries etc. + applicationRootPath = os.path.dirname(application['path']) + options['cwd'] = applicationRootPath + + # Ensure subprocess is detached so closing connect will not also + # close launched applications. + if sys.platform == 'win32': + options['creationflags'] = subprocess.CREATE_NEW_CONSOLE + else: + options['preexec_fn'] = os.setsid + + launchData = dict( + command=command, + options=options, + application=application, + context=context + ) + ftrack.EVENT_HUB.publish( + ftrack.Event( + topic='ftrack.connect.application.launch', + data=launchData + ), + synchronous=True + ) + ftrack_connect.session.get_shared_session().event_hub.publish( + ftrack_api.event.base.Event( + topic='ftrack.connect.application.launch', + data=launchData + ), + synchronous=True + ) + + # Reset variables passed through the hook since they might + # have been replaced by a handler. + command = launchData['command'] + options = launchData['options'] + application = launchData['application'] + context = launchData['context'] + + self.logger.debug( + 'Launching {0} with options {1}'.format(command, options) + ) + process = subprocess.Popen(command, **options) + + except (OSError, TypeError): + self.logger.exception( + '{0} application could not be started with command "{1}".' + .format(applicationIdentifier, command) + ) + + success = False + message = '{0} application could not be started.'.format( + application['label'] + ) + + else: + self.logger.debug( + '{0} application started. (pid={1})'.format( + applicationIdentifier, process.pid + ) + ) + + return { + 'success': success, + 'message': message + } diff --git a/pype/ftrack/create_custAttributes_AvalonId.py b/pype/ftrack/create_custAttributes_AvalonId.py new file mode 100644 index 0000000000..01f6d7b4d6 --- /dev/null +++ b/pype/ftrack/create_custAttributes_AvalonId.py @@ -0,0 +1,51 @@ +import ftrack_utils + +import ftrack_api + + +session = ftrack_api.Session( + server_url="https://pype.ftrackapp.com", + api_key="4e01eda0-24b3-4451-8e01-70edc03286be", + api_user="jakub.trllo", +) + +objTypes = set() + +# TODO get all entity types ---- NOT TASK,MILESTONE,LIBRARY --> should be editable!!! +allObjTypes = session.query('ObjectType').all() +for object in range(len(allObjTypes)): + index = len(allObjTypes)-object-1 + + if (str(allObjTypes[index]['name']) in ['Task','Milestone','Library']): + allObjTypes.pop(index) + +for k in allObjTypes: + print(k['name']) + +# Name & Label for export Avalon-mongo ID to Ftrack +# allCustAttr = session.query('CustomAttributeConfiguration').all() +# curCustAttr = [] +# for ca in allCustAttr: +# curCustAttr.append(ca['key']) +# +# custAttrName = 'avalon_mongo_id' +# custAttrLabel = 'Avalon/Mongo Id' +# custAttrType = session.query('CustomAttributeType where name is "text"').one() +# # TODO WHICH SECURITY ROLE IS RIGHT +# custAttrSecuRole = session.query('SecurityRole').all() + +# for custAttrObjType in objTypes: +# # Create Custom attribute if not exists +# if custAttrName not in curCustAttr: +# session.create('CustomAttributeConfiguration', { +# 'entity_type': 'task', +# 'object_type_id': custAttrObjType['id'], +# 'type': custAttrType, +# 'label': custAttrLabel, +# 'key': custAttrName, +# 'default': '', +# 'write_security_roles': custAttrSecuRole, +# 'read_security_roles': custAttrSecuRole, +# 'config': json.dumps({'markdown': False}), +# }) +# session.commit() diff --git a/pype/ftrack/events/file_version_statuses.py b/pype/ftrack/events/file_version_statuses.py new file mode 100644 index 0000000000..0f8ce11d25 --- /dev/null +++ b/pype/ftrack/events/file_version_statuses.py @@ -0,0 +1,38 @@ +# import ftrack_api as local session +import ftrack_api +from utils import print_entity_head +# +session = ftrack_api.Session() + +# ---------------------------------- + + +def file_version_statuses(event): + '''Set new version status to data if version matches given types''' + + # start of event procedure ---------------------------------- + for entity in event['data'].get('entities', []): + + # Filter to new assetversions + if (entity['entityType'] == 'assetversion' + and entity['action'] == 'add'): + + print "\n\nevent script: {}".format(__file__) + print_entity_head.print_entity_head(entity, session) + + version = session.get('AssetVersion', entity['entityId']) + asset_type = version['asset']['type']['name'] + file_status = session.query( + 'Status where name is "{}"'.format('data')).one() + + # Setting task status + try: + if asset_type.lower() in ['cam', 'cache', 'rig', 'scene']: + version['status'] = file_status + except Exception as e: + print '!!! status couldnt be set [ {} ]'.format(e) + else: + print '>>> updated to [ {} ]'.format(file_status['name']) + + session.commit() + # end of event procedure ---------------------------------- diff --git a/pype/ftrack/events/new_task_update.py b/pype/ftrack/events/new_task_update.py new file mode 100644 index 0000000000..1b114e6b6a --- /dev/null +++ b/pype/ftrack/events/new_task_update.py @@ -0,0 +1,81 @@ +# import ftrack_api as local session +import ftrack_api +import operator +from utils import print_entity_head +# +session = ftrack_api.Session() + +# ---------------------------------- + + +def new_task_update(event): + ''' Set next task to ready when previous task is \ + completed. ''' + + # start of event procedure ---------------------------------- + def get_next_task(task): + parent = task['parent'] + # tasks = parent['tasks'] + tasks = parent['children'] + + def sort_types(types): + data = {} + for t in types: + data[t] = t.get('sort') + + data = sorted(data.items(), key=operator.itemgetter(1)) + results = [] + for item in data: + results.append(item[0]) + return results + + types_sorted = sort_types(session.query('Type')) + next_types = None + for t in types_sorted: + if t['id'] == task['type_id']: + next_types = types_sorted[(types_sorted.index(t) + 1):] + + for nt in next_types: + for t in tasks: + if nt['id'] == t['type_id']: + return t + + return None + + for entity in event['data'].get('entities', []): + + if (entity['entityType'] == 'task' and 'statusid' in entity['keys']): + + print "\n\nevent script: {}".format(__file__) + print_entity_head.print_entity_head(entity, session) + + task = session.get('Task', entity['entityId']) + + status = session.get('Status', + entity['changes']['statusid']['new']) + state = status['state']['name'] + + next_task = get_next_task(task) + + # Setting next task to NOT STARTED, if on NOT READY + if next_task and state == 'Done': + if next_task['status']['name'].lower() == 'not ready': + + # Get path to task + path = task['name'] + for p in task['ancestors']: + path = p['name'] + '/' + path + + # Setting next task status + try: + status_to_set = session.query( + 'Status where name is "{}"'.format('Ready')).one() + next_task['status'] = status_to_set + except Exception as e: + print '!!! [ {} ] status couldnt be set: [ {} ]'.format( + path, e) + else: + print '>>> [ {} ] updated to [ Ready ]'.format(path) + + session.commit() + # end of event procedure ---------------------------------- diff --git a/pype/ftrack/events/radio_buttons.py b/pype/ftrack/events/radio_buttons.py new file mode 100644 index 0000000000..0b7aac952a --- /dev/null +++ b/pype/ftrack/events/radio_buttons.py @@ -0,0 +1,39 @@ +# import ftrack_api as local session +import ftrack_api +from utils import print_entity_head +# +session = ftrack_api.Session() + +# ---------------------------------- + + +def radio_buttons(event): + '''Provides a readio button behaviour to any bolean attribute in + radio_button group.''' + + # start of event procedure ---------------------------------- + for entity in event['data'].get('entities', []): + + if entity['entityType'] == 'assetversion': + + print "\n\nevent script: {}".format(__file__) + print_entity_head.print_entity_head(entity, session) + + group = session.query( + 'CustomAttributeGroup where name is "radio_button"').one() + radio_buttons = [] + for g in group['custom_attribute_configurations']: + radio_buttons.append(g['key']) + + for key in entity['keys']: + if (key in radio_buttons and entity['changes'] is not None): + if entity['changes'][key]['new'] == '1': + version = session.get('AssetVersion', + entity['entityId']) + asset = session.get('Asset', entity['parentId']) + for v in asset['versions']: + if version is not v: + v['custom_attributes'][key] = 0 + + session.commit() + # end of event procedure ---------------------------------- diff --git a/pype/ftrack/events/test_event.py b/pype/ftrack/events/test_event.py new file mode 100644 index 0000000000..3be53492d4 --- /dev/null +++ b/pype/ftrack/events/test_event.py @@ -0,0 +1,28 @@ +# import ftrack_api as local session +import ftrack_api +from utils import print_entity_head +# +session = ftrack_api.Session() + +# ---------------------------------- + + +def test_event(event): + '''just a testing event''' + + # start of event procedure ---------------------------------- + for entity in event['data'].get('entities', []): + if entity['entityType'] == 'task' and entity['action'] == 'update': + + print "\n\nevent script: {}".format(__file__) + print_entity_head.print_entity_head(entity, session) + + # for k in task.keys(): + # print k, task[k] + # print '\n' + # print task['assignments'] + + for e in entity.keys(): + print '{0}: {1}'.format(e, entity[e]) + + # end of event procedure ---------------------------------- diff --git a/pype/ftrack/events/thumbnail_updates.py b/pype/ftrack/events/thumbnail_updates.py new file mode 100644 index 0000000000..5b22c18345 --- /dev/null +++ b/pype/ftrack/events/thumbnail_updates.py @@ -0,0 +1,43 @@ +import ftrack_api +from utils import print_entity_head +# +session = ftrack_api.Session() + +# ---------------------------------- + + +def thumbnail_updates(event): + '''Update thumbnails automatically''' + + # start of event procedure ---------------------------------- + for entity in event['data'].get('entities', []): + + # update created task thumbnail with first parent thumbnail + if entity['entityType'] == 'task' and entity['action'] == 'add': + + print "\n\nevent script: {}".format(__file__) + print_entity_head.print_entity_head(entity, session) + + task = session.get('TypedContext', entity['entityId']) + parent = task['parent'] + + if parent.get('thumbnail') and not task.get('thumbnail'): + task['thumbnail'] = parent['thumbnail'] + print '>>> Updated thumbnail on [ %s/%s ]'.format( + parent['name'], task['name']) + + # Update task thumbnail from published version + if entity['entityType'] == 'assetversion' and entity['action'] == 'encoded': + + version = session.get('AssetVersion', entity['entityId']) + thumbnail = version.get('thumbnail') + task = version['task'] + + if thumbnail: + task['thumbnail'] = thumbnail + task['parent']['thumbnail'] = thumbnail + print '>>> Updating thumbnail for task and shot [ {} ]'.format( + task['name']) + + session.commit() + # end of event procedure ---------------------------------- diff --git a/pype/ftrack/events/version_to_task_status.py b/pype/ftrack/events/version_to_task_status.py new file mode 100644 index 0000000000..bdb0199045 --- /dev/null +++ b/pype/ftrack/events/version_to_task_status.py @@ -0,0 +1,62 @@ +# import ftrack_api as local session +import ftrack_api +from utils import print_entity_head +# +session = ftrack_api.Session() + +# ---------------------------------- + + +def version_to_task_status(event): + '''Push version status to task''' + + # start of event procedure ---------------------------------- + for entity in event['data'].get('entities', []): + # Filter non-assetversions + if (entity['entityType'] == 'assetversion' + and 'statusid' in entity['keys']): + + print "\n\nevent script: {}".format(__file__) + print_entity_head.print_entity_head(entity, session) + + version = session.get('AssetVersion', entity['entityId']) + version_status = session.get('Status', + entity['changes']['statusid']['new']) + task_status = version_status + task = version['task'] + print '>>> version status: [ {} ]'.format(version_status['name']) + + status_to_set = None + # Filter to versions with status change to "render complete" + if version_status['name'].lower() == 'reviewed': + status_to_set = 'Change requested' + + if version_status['name'].lower() == 'approved': + status_to_set = 'Complete' + if task['type']['name'] == 'Lighting': + status_to_set = 'To render' + print '>>> status to set: [ {} ]'.format(status_to_set) + + if status_to_set is not None: + task_status = session.query( + 'Status where name is "{}"'.format(status_to_set)).one() + # + # Proceed if the task status was set + if task_status: + # Get path to task + path = task['name'] + for p in task['ancestors']: + path = p['name'] + '/' + path + + # Setting task status + try: + task['status'] = task_status + session.commit() + except Exception as e: + print '!!! [ {} ] status couldnt be set: [ {} ]'.format( + path, e) + # print '{} status couldnt be set: {}' + else: + print '>>> [ {} ] updated to [ {} ]'.format( + path, task_status['name']) + # end of event procedure ---------------------------------- diff --git a/pype/ftrack/ftrack_utils.py b/pype/ftrack/ftrack_utils.py index 9b2e122df9..ce527ecc78 100644 --- a/pype/ftrack/ftrack_utils.py +++ b/pype/ftrack/ftrack_utils.py @@ -31,7 +31,7 @@ def deleteAssetsFromShotByName(shotId, assNm=None): if nm == assNm: a.delete() - +# Created as action def killRunningTasks(tm=None): import datetime import ftrack_api @@ -58,47 +58,6 @@ def killRunningTasks(tm=None): print('Complete') - -def createCustomAttr(): - - import ftrack_api - - session = ftrack_api.Session() - objTypes = set() # for custom attribute creation - - # TODO get all entity types ---- NOT TASK - # objTypes.add(session.query('ObjectType where name is ' + entity.entity_type).one()) - - # Name & Label for export Avalon-mongo ID to Ftrack - allCustAttr = session.query('CustomAttributeConfiguration').all() - curCustAttr = [] - for ca in allCustAttr: - curCustAttr.append(ca['key']) - - custAttrName = 'avalon_mongo_id' - custAttrLabel = 'Avalon/Mongo Id' - custAttrType = session.query('CustomAttributeType where name is "text"').one() - # TODO WHICH SECURITY ROLE IS RIGHT - custAttrSecuRole = session.query('SecurityRole').all() - - for custAttrObjType in objTypes: - # custAttrObjType = session.query('ObjectType where name is ' + entity.entity_type).one() - # Create Custom attribute if not exists - if custAttrName not in curCustAttr: - session.create('CustomAttributeConfiguration', { - 'entity_type': 'task', - 'object_type_id': custAttrObjType['id'], - 'type': custAttrType, - 'label': custAttrLabel, - 'key': custAttrName, - 'default': '', - 'write_security_roles': custAttrSecuRole, - 'read_security_roles': custAttrSecuRole, - 'config': json.dumps({'markdown': False}), - }) - session.commit() - - def checkRegex(): # _handle_result -> would be solution? # """ TODO Check if name of entities match REGEX""" diff --git a/pype/ftrack/test_events/test_event.py b/pype/ftrack/test_events/test_event.py new file mode 100644 index 0000000000..e662fa8011 --- /dev/null +++ b/pype/ftrack/test_events/test_event.py @@ -0,0 +1,16 @@ +# import ftrack_api as local session +import ftrack_api +# +session = ftrack_api.Session() + +# ---------------------------------- + + +def test_event(event): + '''just a testing event''' + # start of event procedure ---------------------------------- + for entity in event['data'].get('entities', []): + print(100*"_") + print(entity['changes']) + + # end of event procedure ---------------------------------- diff --git a/pype/ftrack/test_events/test_event2.py b/pype/ftrack/test_events/test_event2.py new file mode 100644 index 0000000000..7241ebe65d --- /dev/null +++ b/pype/ftrack/test_events/test_event2.py @@ -0,0 +1,16 @@ +# import ftrack_api as local session +import ftrack_api +# +session = ftrack_api.Session() + +# ---------------------------------- + + +def test_event(event): + '''just a testing event''' + # start of event procedure ---------------------------------- + for entity in event['data'].get('entities', []): + print(100*"_") + print(entity['keys']) + + # end of event procedure ---------------------------------- diff --git a/pype/vendor/ftrack_action_handler/action.py b/pype/vendor/ftrack_action_handler/action.py index 9835ce95f3..d248f23697 100644 --- a/pype/vendor/ftrack_action_handler/action.py +++ b/pype/vendor/ftrack_action_handler/action.py @@ -23,6 +23,7 @@ class BaseAction(object): variant = None identifier = None description = None + icon = None def __init__(self, session): '''Expects a ftrack_api.Session instance''' @@ -77,7 +78,7 @@ class BaseAction(object): 'variant': self.variant, 'description': self.description, 'actionIdentifier': self.identifier, - + 'icon': self.icon, }] } diff --git a/pype/vendor/ftrack_action_handler/appaction.py b/pype/vendor/ftrack_action_handler/appaction.py new file mode 100644 index 0000000000..b1b76c3f10 --- /dev/null +++ b/pype/vendor/ftrack_action_handler/appaction.py @@ -0,0 +1,240 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2017 ftrack + +import logging +import getpass +import ftrack_api + + +class AppAction(object): + '''Custom Action base class + +