From 06f1dd9057693501fd89b2cedd555ef5b8c999b6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 31 Oct 2018 15:19:47 +0100 Subject: [PATCH 01/16] Sync to Avalon count with duplication name possibility --- pype/ftrack/actions/action_syncToAvalon.py | 46 ++++++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/pype/ftrack/actions/action_syncToAvalon.py b/pype/ftrack/actions/action_syncToAvalon.py index a2787e7a38..3516469245 100644 --- a/pype/ftrack/actions/action_syncToAvalon.py +++ b/pype/ftrack/actions/action_syncToAvalon.py @@ -81,7 +81,6 @@ class SyncToAvalon(BaseAction): 'schema': 'avalon-core:config-1.0', 'tasks': [{'name': ''}], 'apps': apps, - # TODO redo work!!! 'template': {'work': '','publish':''} } @@ -90,6 +89,7 @@ class SyncToAvalon(BaseAction): # --- Create project and assets in Avalon --- io.install() + ## ----- PROJECT ------ # If project don't exists -> ELSE if (io.find_one({'type': 'project', 'name': entityProj['full_name']}) is None): @@ -110,10 +110,12 @@ class SyncToAvalon(BaseAction): # If entity is Project or have only 1 entity kill action if (len(eLinks) > 1) and not (eLinks[-1]['type'] in ['Project']): + ## ----- ASSETS ------ + # Presets: # 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 + # Get list of assets without project assets = [] for i in range(1, len(eLinks)): assets.append(eLinks[i]) @@ -134,21 +136,30 @@ class SyncToAvalon(BaseAction): tasks.append(child['name']) data.update({'tasks': tasks}) - if (io.find_one({'type': 'asset', 'name': asset['name']}) is None): - # Create asset in DB + # Try to find asset in current database + avalon_asset = io.find_one({'type': 'asset', 'name': asset['name']}) + # Create if don't exists + if avalon_asset is None: inventory.create_asset(asset['name'], silo, data, projectId) print("Asset "+asset['name']+" - created") + # Raise error if it seems to be different ent. with same name + elif (avalon_asset['data']['ftrackId'] != data['ftrackId'] or + avalon_asset['data']['visualParent'] != data['visualParent'] or + avalon_asset['data']['parents'] != data['parents']): + raise ValueError('Possibility of entity name duplication: {}'.format(asset['name'])) + # Else update info else: io.update_many({'type': 'asset','name': asset['name']}, - {'$set':{'data':data}}) + {'$set':{'data':data, 'silo': silo}}) # TODO check if is asset in same folder!!! ???? FEATURE FOR FUTURE - print("Asset "+asset["name"]+" - already exist") + print("Asset "+asset["name"]+" - updated") + # Get parent ID and store it to data parentId = io.find_one({'type': 'asset', 'name': asset['name']})['_id'] data.update({'visualParent': parentId, 'parents': folderStruct}) folderStruct.append(asset['name']) - + ## 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 custAttrName in entity['custom_attributes'] and entity['custom_attributes'][custAttrName] is '': entity['custom_attributes'][custAttrName] = str(parentId) @@ -156,6 +167,8 @@ class SyncToAvalon(BaseAction): io.uninstall() def launch(self, session, entities, event): + message = "" + # JOB SETTINGS userId = event['source']['user']['id'] user = session.query('User where id is ' + userId).one() @@ -170,10 +183,7 @@ class SyncToAvalon(BaseAction): 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" + #TODO AVALON_PROJECTS, AVALON_ASSET, AVALON_SILO should be set up otherwise console log shows avalon debug importable = [] def getShotAsset(entity): @@ -186,7 +196,7 @@ class SyncToAvalon(BaseAction): for child in childrens: getShotAsset(child) - # get all entities separately + # get all entities separately/unique for entity in entities: entity_type, entity_id = entity act_ent = session.get(entity_type, entity_id) @@ -203,8 +213,18 @@ class SyncToAvalon(BaseAction): job['status'] = 'failed' print('During synchronization to Avalon went something wrong!') print(e) + message = str(e) - return True + if len(message) > 0: + return { + 'success': False, + 'message': message + } + + return { + 'success': True, + 'message': "Synchronization was successfull" + } def register(session, **kw): From ab6f07e28cc3199307d100c393445a17bf3778ec Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 1 Nov 2018 17:46:09 +0100 Subject: [PATCH 02/16] Apps Action runs also timer in FT. New actions Thumb to child/parent, sort review and *open_folder --- .../actions/action_client_review_sort.py | 117 +++++ .../actions/action_createCustomAttributes.py | 1 - pype/ftrack/actions/action_open_folder.py | 170 +++++++ pype/ftrack/actions/action_syncToAvalon.py | 1 - pype/ftrack/actions/action_test.py | 9 +- pype/ftrack/actions/action_thumbToChildern.py | 125 +++++ pype/ftrack/actions/action_thumbToParent.py | 134 ++++++ pype/ftrack/actions/ft_utils.py | 428 ++++++++++++++++++ pype/ftrack/actions/ftrack_action_handler.py | 10 + pype/ftrack/ftrack_utils.py | 171 ++++--- pype/ftrack/test_events/test_event.py | 16 - pype/ftrack/test_events/test_event2.py | 16 - 12 files changed, 1111 insertions(+), 87 deletions(-) create mode 100644 pype/ftrack/actions/action_client_review_sort.py create mode 100644 pype/ftrack/actions/action_open_folder.py create mode 100644 pype/ftrack/actions/action_thumbToChildern.py create mode 100644 pype/ftrack/actions/action_thumbToParent.py create mode 100644 pype/ftrack/actions/ft_utils.py delete mode 100644 pype/ftrack/test_events/test_event.py delete mode 100644 pype/ftrack/test_events/test_event2.py diff --git a/pype/ftrack/actions/action_client_review_sort.py b/pype/ftrack/actions/action_client_review_sort.py new file mode 100644 index 0000000000..4df73178e9 --- /dev/null +++ b/pype/ftrack/actions/action_client_review_sort.py @@ -0,0 +1,117 @@ +import sys +import argparse +import logging +import os +import getpass + +import ftrack_api +from ftrack_action_handler import BaseAction + + + +class ClientReviewSort(BaseAction): + '''Custom action.''' + + #: Action identifier. + identifier = 'client.review.sort' + + #: Action label. + label = 'Sort Review' + + + def validateSelection(self, entities): + '''Return true if the selection is valid. ''' + + if len(entities) == 0: + return False + + return True + + + def discover(self, session, entities, event): + '''Return action config if triggered on a single selection.''' + + selection = event['data']['selection'] + # this action will only handle a single version. + if (not self.validateSelection(entities) or + selection[0]['entityType'] != 'reviewsession'): + return False + + return True + + + def launch(self, session, entities, event): + + entity_type, entity_id = entities[0] + entity = session.get(entity_type, entity_id) + + # Get all objects from Review Session and all 'sort order' possibilities + obj_list = [] + sort_order_list = [] + for obj in entity['review_session_objects']: + obj_list.append(obj) + sort_order_list.append(obj['sort_order']) + + # Sort criteria + obj_list = sorted(obj_list, key=lambda k: k['asset_version']['task']['name']) + obj_list = sorted(obj_list, key=lambda k: k['version']) + obj_list = sorted(obj_list, key=lambda k: k['name']) + + # Set 'sort order' to sorted list, so they are sorted in Ftrack also + for i in range(len(obj_list)): + obj_list[i]['sort_order'] = sort_order_list[i] + + session.commit() + + return { + 'success': True, + 'message': 'Client Review sorted!' + } + + +def register(session, **kw): + '''Register action. Called when used as an event plugin.''' + if not isinstance(session, ftrack_api.session.Session): + return + + action_handler = ClientReviewSort(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:])) diff --git a/pype/ftrack/actions/action_createCustomAttributes.py b/pype/ftrack/actions/action_createCustomAttributes.py index 9133709c13..70085f9e69 100644 --- a/pype/ftrack/actions/action_createCustomAttributes.py +++ b/pype/ftrack/actions/action_createCustomAttributes.py @@ -215,7 +215,6 @@ def register(session, **kw): action_handler = AvalonIdAttribute(session) action_handler.register() - print("----- action - <" + action_handler.__class__.__name__ + "> - Has been registered -----") def main(arguments=None): diff --git a/pype/ftrack/actions/action_open_folder.py b/pype/ftrack/actions/action_open_folder.py new file mode 100644 index 0000000000..b301d36ae7 --- /dev/null +++ b/pype/ftrack/actions/action_open_folder.py @@ -0,0 +1,170 @@ +import sys +import argparse +import logging +import getpass +import subprocess +import os + +import ftrack_api +from ftrack_action_handler import BaseAction +import ft_utils + +class openFolder(BaseAction): + '''Open folders action''' + + #: Action identifier. + identifier = 'open.folders' + #: Action label. + label = 'Open Folders' + #: Action Icon. + icon = "https://cdn3.iconfinder.com/data/icons/stroke/53/Open-Folder-256.png" + + + def validateSelection(self, selection): + '''Return true if the selection is valid. ''' + + if len(selection) == 0 or selection[0]['entityType'] in ['assetversion', 'Component']: + return False + + return True + + def discover(self, session, entities, event): + selection = event['data']['selection'] + + # validate selection, and only return action if it is valid. + return self.validateSelection(selection) + + def get_paths(self, entity): + '''Prepare all the paths for the entity. + + This function uses custom module to deal with paths. + You will need to replace it with your logic. + ''' + + root = entity['project']['root'] + entity_type = entity.entity_type.lower() + + if entity_type == 'task': + if entity['parent'].entity_type == 'Asset Build': + templates = ['asset.task'] + else: + templates = ['shot.task'] + + elif entity_type in ['shot', 'folder', 'sequence', 'episode']: + templates = ['shot'] + + elif entity_type in ['asset build', 'library']: + templates = ['asset'] + + paths = ft_utils.getPathsYaml(entity, + templateList=templates, + root=root) + return paths + + def launch(self, session, entities, event): + '''Callback method for action.''' + selection = event['data'].get('selection', []) + self.logger.info(u'Launching action with selection \ + {0}'.format(selection)) + + # Prepare lists to keep track of failures and successes + fails = [] + hits = set([]) + + for entity in entities: + entity_type, entity_id = entity + entity = session.get(entity_type, entity_id) + + # Get paths base on the entity. + # This function needs to be chagned to fit your path logic + paths = self.get_paths(entity) + + # For each path, check if it exists on the disk and try opening it + for path in paths: + if os.path.isdir(path): + self.logger.info('Opening: ' + path) + + # open the folder + if sys.platform == 'darwin': + subprocess.Popen(['open', '--', path]) + elif sys.platform == 'linux2': + subprocess.Popen(['gnome-open', '--', path]) + elif sys.platform == 'win32': + subprocess.Popen(['explorer', path]) + + # add path to list of hits + hits.add(entity['name']) + + # Add entity to fails list if no folder could be openned for it + if entity['name'] not in hits: + fails.append(entity['name']) + + # Inform user of the result + if len(hits) == 0: + return { + 'success': False, + 'message': 'No folders found for: {}'.format(', '.join(fails)) + } + + if len(fails) > 0: + return { + 'success': True, + 'message': 'No folders found for: {}'.format(', '.join(fails)) + } + + return { + 'success': True, + 'message': 'Opening folders' + } + + +def register(session, **kw): + '''Register action. Called when used as an event 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 = openFolder(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:])) diff --git a/pype/ftrack/actions/action_syncToAvalon.py b/pype/ftrack/actions/action_syncToAvalon.py index 3516469245..ed65396f37 100644 --- a/pype/ftrack/actions/action_syncToAvalon.py +++ b/pype/ftrack/actions/action_syncToAvalon.py @@ -238,7 +238,6 @@ def register(session, **kw): action_handler = SyncToAvalon(session) action_handler.register() - print("----- action - <" + action_handler.__class__.__name__ + "> - Has been registered -----") def main(arguments=None): diff --git a/pype/ftrack/actions/action_test.py b/pype/ftrack/actions/action_test.py index a5eca5dfac..aae44aad44 100644 --- a/pype/ftrack/actions/action_test.py +++ b/pype/ftrack/actions/action_test.py @@ -27,7 +27,7 @@ class TestAction(BaseAction): def validate_selection(self, session, entities): '''Return if *entities* is a valid selection.''' - pass + return True def discover(self, session, entities, event): @@ -38,6 +38,13 @@ class TestAction(BaseAction): def launch(self, session, entities, event): + for entity in entities: + entity_type, entity_id = entity + entity = session.get(entity_type, entity_id) + + import ft_utils + print(ft_utils.getNewContext(entity)) + return True diff --git a/pype/ftrack/actions/action_thumbToChildern.py b/pype/ftrack/actions/action_thumbToChildern.py new file mode 100644 index 0000000000..8a2889aae0 --- /dev/null +++ b/pype/ftrack/actions/action_thumbToChildern.py @@ -0,0 +1,125 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2015 Milan Kolar + +import sys +import argparse +import logging +import getpass +import json + +import ftrack_api +from ftrack_action_handler import BaseAction + +class ThumbToChildren(BaseAction): + '''Custom action.''' + + # Action identifier + identifier = 'thumb.to.children' + # Action label + label = 'Thumbnail to Children' + # Action icon + icon = "https://cdn3.iconfinder.com/data/icons/transfers/100/239322-download_transfer-128.png" + + def validateSelection(self, selection): + '''Return true if the selection is valid. + + Legacy plugins can only be started from a single Task. ''' + + if len(selection) > 0: + if selection[0]['entityType'] in ['assetversion', 'task']: + return True + + return False + + def discover(self, session, entities, event): + '''Return action config if triggered on asset versions.''' + selection = event['data']['selection'] + + # validate selection, and only return action if it is valid. + return self.validateSelection(selection) + + + def launch(self, session, entities, event): + '''Callback method for action.''' + + 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': 'Push thumbnails to Childrens' + }) + }) + + try: + for entity in entities: + entity_type, entity_id = entity + entity = session.get(entity_type, entity_id) + + thumbid = entity['thumbnail_id'] + if thumbid: + for child in entity['children']: + child['thumbnail_id'] = thumbid + + # inform the user that the job is done + job['status'] = 'done' + session.commit() + except: + # fail the job if something goes wrong + job['status'] = 'failed' + raise + + return { + 'success': True, + 'message': 'Created job for updating thumbnails!' + } + + + +def register(session, **kw): + '''Register action. Called when used as an event plugin.''' + if not isinstance(session, ftrack_api.session.Session): + return + + action_handler = ThumbToChildren(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:])) diff --git a/pype/ftrack/actions/action_thumbToParent.py b/pype/ftrack/actions/action_thumbToParent.py new file mode 100644 index 0000000000..08c6c24674 --- /dev/null +++ b/pype/ftrack/actions/action_thumbToParent.py @@ -0,0 +1,134 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2015 Milan Kolar + +import sys +import argparse +import logging +import getpass +import json +import ftrack_api +from ftrack_action_handler import BaseAction + +class ThumbToParent(BaseAction): + '''Custom action.''' + + # Action identifier + identifier = 'thumb.to.parent' + # Action label + label = 'Thumbnail to Parent' + # Action icon + icon = "https://cdn3.iconfinder.com/data/icons/transfers/100/239419-upload_transfer-512.png" + + + def validateSelection(self, selection): + '''Return true if the selection is valid. + + Legacy plugins can only be started from a single Task. + + ''' + if len(selection) > 0: + if selection[0]['entityType'] in ['assetversion', 'task']: + return True + + return False + + def discover(self, session, entities, event): + '''Return action config if triggered on asset versions.''' + selection = event['data']['selection'] + # validate selection, and only return action if it is valid. + return self.validateSelection(selection) + + + def launch(self, session, entities, event): + '''Callback method for action.''' + + 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': 'Push thumbnails to parents' + }) + }) + + try: + for entity in entities: + entity_type, entity_id = entity + entity = session.get(entity_type, entity_id) + + if entity.entity_type.lower() == 'assetversion': + try: + parent = entity['task'] + except: + par_ent = entity['link'][-2] + parent = session.get(par_ent['type'], par_ent['id']) + + elif entity.entity_type.lower() == 'task': + parent = entity['parent'] + + thumbid = entity['thumbnail_id'] + + if parent and thumbid: + parent['thumbnail_id'] = thumbid + + # inform the user that the job is done + job['status'] = 'done' + session.commit() + except: + # fail the job if something goes wrong + job['status'] = 'failed' + raise + + return { + 'success': True, + 'message': 'Created job for updating thumbnails!' + } + + +def register(session, **kw): + '''Register action. Called when used as an event plugin.''' + if not isinstance(session, ftrack_api.session.Session): + return + + action_handler = ThumbToParent(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:])) diff --git a/pype/ftrack/actions/ft_utils.py b/pype/ftrack/actions/ft_utils.py new file mode 100644 index 0000000000..4b30253f9d --- /dev/null +++ b/pype/ftrack/actions/ft_utils.py @@ -0,0 +1,428 @@ +import os +import operator +import ftrack_api +import collections +import sys +import json +import base64 + +# sys.path.append(os.path.dirname(os.path.dirname(__file__))) +# from ftrack_kredenc.lucidity.vendor import yaml +# from ftrack_kredenc import lucidity +# +# +# def get_ftrack_connect_path(): +# +# ftrack_connect_root = os.path.abspath(os.getenv('FTRACK_CONNECT_PACKAGE')) +# +# return ftrack_connect_root +# +# +# def from_yaml(filepath): +# ''' Parse a Schema from a YAML file at the given *filepath*. +# ''' +# with open(filepath, 'r') as f: +# data = yaml.safe_load(f) +# return data +# +# +# def get_task_enviro(entity, environment=None): +# +# context = get_context(entity) +# +# if not environment: +# environment = {} +# +# for key in context: +# os.environ[key.upper()] = context[key]['name'] +# environment[key.upper()] = context[key]['name'] +# +# if key == 'Project': +# os.putenv('PROJECT_ROOT', context[key]['root']) +# os.environ['PROJECT_ROOT'] = context[key]['root'] +# environment['PROJECT_ROOT'] = context[key]['root'] +# print('PROJECT_ROOT: ' + context[key]['root']) +# print(key + ': ' + context[key]['name']) +# +# return environment +# +# +# def get_entity(): +# decodedEventData = json.loads( +# base64.b64decode( +# os.environ.get('FTRACK_CONNECT_EVENT') +# ) +# ) +# +# entity = decodedEventData.get('selection')[0] +# +# if entity['entityType'] == 'task': +# return ftrack_api.Task(entity['entityId']) +# else: +# return None +# +# +# def set_env_vars(): +# +# entity = get_entity() +# +# if entity: +# if not os.environ.get('project_root'): +# enviro = get_task_enviro(entity) +# +# print(enviro) +# +# +def get_context(entity): + + entityName = entity['name'] + entityId = entity['id'] + entityType = entity.entity_type + entityDescription = entity['description'] + + print(100*"*") + for k in entity['ancestors']: + print(k['name']) + print(100*"*") + hierarchy = entity.getParents() + + ctx = collections.OrderedDict() + + if entity.get('entityType') == 'task' and entityType == 'Task': + taskType = entity.getType().getName() + entityDic = { + 'type': taskType, + 'name': entityName, + 'id': entityId, + 'description': entityDescription + } + elif entity.get('entityType') == 'task': + entityDic = { + 'name': entityName, + 'id': entityId, + 'description': entityDescription + } + + ctx[entityType] = entityDic + + folder_counter = 0 + + for ancestor in hierarchy: + tempdic = {} + if isinstance(ancestor, ftrack_api.Component): + # Ignore intermediate components. + continue + + tempdic['name'] = ancestor.getName() + tempdic['id'] = ancestor.getId() + + try: + objectType = ancestor.getObjectType() + tempdic['description'] = ancestor.getDescription() + except AttributeError: + objectType = 'Project' + tempdic['description'] = '' + + if objectType == 'Asset Build': + tempdic['type'] = ancestor.getType().get('name') + objectType = objectType.replace(' ', '_') + elif objectType == 'Project': + tempdic['code'] = tempdic['name'] + tempdic['name'] = ancestor.get('fullname') + tempdic['root'] = ancestor.getRoot() + + if objectType == 'Folder': + objectType = objectType + str(folder_counter) + folder_counter += 1 + ctx[objectType] = tempdic + + return ctx + + +def getNewContext(entity): + + parents = [] + item = entity + while True: + item = item['parent'] + if not item: + break + parents.append(item) + + ctx = collections.OrderedDict() + + entityDic = { + 'name': entity['name'], + 'id': entity['id'], + } + try: + entityDic['type'] = entity['type']['name'] + except: + pass + + ctx[entity['object_type']['name']] = entityDic + + print(100*"-") + for p in parents: + print(p) + # add all parents to the context + for parent in parents: + tempdic = {} + if not parent.get('project_schema'): + tempdic = { + 'name': parent['full_name'], + 'code': parent['name'], + 'id': parent['id'], + } + tempdic = { + 'name': parent['name'], + 'id': parent['id'], + } + object_type = parent['object_type']['name'] + + ctx[object_type] = tempdic + + # add project to the context + project = entity['project'] + ctx['Project'] = { + 'name': project['full_name'], + 'code': project['name'], + 'id': project['id'], + 'root': project['root'], + }, + + return ctx +# +# +# def get_frame_range(): +# +# entity = get_entity() +# entityType = entity.getObjectType() +# environment = {} +# +# if entityType == 'Task': +# try: +# environment['FS'] = str(int(entity.getFrameStart())) +# except Exception: +# environment['FS'] = '1' +# try: +# environment['FE'] = str(int(entity.getFrameEnd())) +# except Exception: +# environment['FE'] = '1' +# else: +# try: +# environment['FS'] = str(int(entity.getFrameStart())) +# except Exception: +# environment['FS'] = '1' +# try: +# environment['FE'] = str(int(entity.getFrameEnd())) +# except Exception: +# environment['FE'] = '1' +# +# +# def get_asset_name_by_id(id): +# for t in ftrack_api.getAssetTypes(): +# try: +# if t.get('typeid') == id: +# return t.get('name') +# except: +# return None +# +# +# def get_status_by_name(name): +# statuses = ftrack_api.getTaskStatuses() +# +# result = None +# for s in statuses: +# if s.get('name').lower() == name.lower(): +# result = s +# +# return result +# +# +# 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 +# +# +# def get_next_task(task): +# shot = task.getParent() +# tasks = shot.getTasks() +# +# types_sorted = sort_types(ftrack_api.getTaskTypes()) +# +# next_types = None +# for t in types_sorted: +# if t.get('typeid') == task.get('typeid'): +# try: +# next_types = types_sorted[(types_sorted.index(t) + 1):] +# except: +# pass +# +# for nt in next_types: +# for t in tasks: +# if nt.get('typeid') == t.get('typeid'): +# return t +# +# return None +# +# +# def get_latest_version(versions): +# latestVersion = None +# if len(versions) > 0: +# versionNumber = 0 +# for item in versions: +# if item.get('version') > versionNumber: +# versionNumber = item.getVersion() +# latestVersion = item +# return latestVersion +# +# +# def get_thumbnail_recursive(task): +# if task.get('thumbid'): +# thumbid = task.get('thumbid') +# return ftrack_api.Attachment(id=thumbid) +# if not task.get('thumbid'): +# parent = ftrack_api.Task(id=task.get('parent_id')) +# return get_thumbnail_recursive(parent) +# +# +# # paths_collected +# +# def getFolderHierarchy(context): +# '''Return structure for *hierarchy*. +# ''' +# +# hierarchy = [] +# for key in reversed(context): +# hierarchy.append(context[key]['name']) +# print(hierarchy) +# +# return os.path.join(*hierarchy[1:-1]) +# +# +def tweakContext(context, include=False): + + for key in context: + if key == 'Asset Build': + context['Asset_Build'] = context.pop(key) + key = 'Asset_Build' + description = context[key].get('description') + if description: + context[key]['description'] = '_' + description + + hierarchy = [] + for key in reversed(context): + hierarchy.append(context[key]['name']) + + if include: + hierarchy = os.path.join(*hierarchy[1:]) + else: + hierarchy = os.path.join(*hierarchy[1:-1]) + + context['ft_hierarchy'] = hierarchy + + +def getSchema(entity): + + project = entity['project'] + schema = project['project_schema']['name'] + + tools = os.path.abspath(os.environ.get('studio_tools')) + + schema_path = os.path.join(tools, 'studio', 'templates', (schema + '_' + project['name'] + '.yml')) + if not os.path.exists(schema_path): + schema_path = os.path.join(tools, 'studio', 'templates', (schema + '.yml')) + if not os.path.exists(schema_path): + schema_path = os.path.join(tools, 'studio', 'templates', 'default.yml') + + schema = lucidity.Schema.from_yaml(schema_path) + + print(schema_path) + return schema + + +# def getAllPathsYaml(entity, root=''): +# +# if isinstance(entity, str) or isinstance(entity, unicode): +# entity = ftrack_api.Task(entity) +# +# context = get_context(entity) +# +# tweakContext(context) +# +# schema = getSchema(entity) +# +# paths = schema.format_all(context) +# paths_collected = [] +# +# for path in paths: +# tweak_path = path[0].replace(" ", '_').replace('\'', '').replace('\\', '/') +# +# tempPath = os.path.join(root, tweak_path) +# path = list(path) +# path[0] = tempPath +# paths_collected.append(path) +# +# return paths_collected +# + +def getPathsYaml(entity, templateList=None, root=None, **kwargs): + ''' + version=None + ext=None + item=None + family=None + subset=None + ''' + + context = get_context(entity) + + if entity.entity_type != 'Task': + tweakContext(context, include=True) + else: + tweakContext(context) + + context.update(kwargs) + + host = sys.executable.lower() + + ext = None + if not context.get('ext'): + if "nuke" in host: + ext = 'nk' + elif "maya" in host: + ext = 'ma' + elif "houdini" in host: + ext = 'hip' + if ext: + context['ext'] = ext + + if not context.get('subset'): + context['subset'] = '' + else: + context['subset'] = '_' + context['subset'] + + schema = getSchema(entity) + paths = schema.format_all(context) + paths_collected = set([]) + for temp_mask in templateList: + for path in paths: + if temp_mask in path[1].name: + path = path[0].lower().replace(" ", '_').replace('\'', '').replace('\\', '/') + path_list = path.split('/') + if path_list[0].endswith(':'): + path_list[0] = path_list[0] + os.path.sep + path = os.path.join(*path_list) + temppath = os.path.join(root, path) + paths_collected.add(temppath) + + return list(paths_collected) diff --git a/pype/ftrack/actions/ftrack_action_handler.py b/pype/ftrack/actions/ftrack_action_handler.py index dd40854d42..80fbaabb5d 100644 --- a/pype/ftrack/actions/ftrack_action_handler.py +++ b/pype/ftrack/actions/ftrack_action_handler.py @@ -72,6 +72,7 @@ class AppAction(object): self._launch ) + def _discover(self, event): args = self._translate_event( self.session, event @@ -218,6 +219,7 @@ class AppAction(object): *event* the unmodified original event ''' + # TODO Delete this line print("Action - {0} ({1}) - just started".format(self.label, self.identifier)) @@ -289,6 +291,13 @@ class AppAction(object): 'message': "We didn't found launcher for {0}".format(self.label) } + # RUN TIMER IN FTRACK + username = event['source']['user']['username'] + user = session.query('User where username is "{}"'.format(username)).one() + task = session.query('Task where id is {}'.format(entity['id'])).one() + print('Starting timer for task: ' + task['name']) + user.start_timer(task, force=True) + return { 'success': True, 'message': "Launching {0}".format(self.label) @@ -404,6 +413,7 @@ class BaseAction(object): ), self._launch ) + print("----- action - <" + self.__class__.__name__ + "> - Has been registered -----") def _discover(self, event): args = self._translate_event( diff --git a/pype/ftrack/ftrack_utils.py b/pype/ftrack/ftrack_utils.py index ce527ecc78..a7bf8efc17 100644 --- a/pype/ftrack/ftrack_utils.py +++ b/pype/ftrack/ftrack_utils.py @@ -6,58 +6,6 @@ import os from pprint import * -def deleteAssetsForTask(taskId): - #taskId = os.environ['FTRACK_TASKID'] - task = ftrack.Task(taskId) - - taskAssets = task.getAssets() - print(taskAssets) - for a in taskAssets: - print(a.getName()) - a.delete() - #shot = task.getParent() - #shotAssets = shot.getAssets() - - -def deleteAssetsFromShotByName(shotId, assNm=None): - if not assNm: - return - - shot = ftrack.Task(shotId) - - shotAssets = shot.getAssets() - for a in shotAssets: - nm = a.getName() - if nm == assNm: - a.delete() - -# Created as action -def killRunningTasks(tm=None): - import datetime - import ftrack_api - - session = ftrack_api.Session() - - # Query all jobs created prior to yesterday which has not yet been completed. - yesterday = datetime.date.today() - datetime.timedelta(days=1) - if tm: - yesterday = tm - print(yesterday) - jobs = session.query( - 'select id, status from Job ' - 'where status in ("queued", "running") and created_at > {0}'.format(yesterday) - ) - - # Update all the queried jobs, setting the status to failed. - for job in jobs: - print(job['created_at']) - print('Changing Job ({}) status: {} -> failed'.format(job['id'], job['status'])) - job['status'] = 'failed' - - session.commit() - - print('Complete') - def checkRegex(): # _handle_result -> would be solution? # """ TODO Check if name of entities match REGEX""" @@ -83,3 +31,122 @@ def checkRegex(): 'message': 'Entity name contains invalid character!' } ) + + +def get_context(entity): + + parents = [] + item = entity + while True: + item = item['parent'] + if not item: + break + parents.append(item) + + ctx = collections.OrderedDict() + folder_counter = 0 + + entityDic = { + 'name': entity['name'], + 'id': entity['id'], + } + try: + entityDic['type'] = entity['type']['name'] + except: + pass + + ctx[entity['object_type']['name']] = entityDic + + + # add all parents to the context + for parent in parents: + tempdic = {} + if not parent.get('project_schema'): + tempdic = { + 'name': parent['name'], + 'id': parent['id'], + } + object_type = parent['object_type']['name'] + + if object_type == 'Folder': + object_type = object_type + str(folder_counter) + folder_counter += 1 + + ctx[object_type] = tempdic + + # add project to the context + project = entity['project'] + ctx['Project'] = { + 'name': project['full_name'], + 'code': project['name'], + 'id': project['id'], + 'root': project['root'] + } + + return ctx + + +def get_status_by_name(name): + statuses = ftrack.getTaskStatuses() + + result = None + for s in statuses: + if s.get('name').lower() == name.lower(): + result = s + + return result + + +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 + + +def get_next_task(task): + shot = task.getParent() + tasks = shot.getTasks() + + types_sorted = sort_types(ftrack.getTaskTypes()) + + next_types = None + for t in types_sorted: + if t.get('typeid') == task.get('typeid'): + try: + next_types = types_sorted[(types_sorted.index(t) + 1):] + except: + pass + + for nt in next_types: + for t in tasks: + if nt.get('typeid') == t.get('typeid'): + return t + + return None + + +def get_latest_version(versions): + latestVersion = None + if len(versions) > 0: + versionNumber = 0 + for item in versions: + if item.get('version') > versionNumber: + versionNumber = item.getVersion() + latestVersion = item + return latestVersion + + +def get_thumbnail_recursive(task): + if task.get('thumbid'): + thumbid = task.get('thumbid') + return ftrack.Attachment(id=thumbid) + if not task.get('thumbid'): + parent = ftrack.Task(id=task.get('parent_id')) + return get_thumbnail_recursive(parent) diff --git a/pype/ftrack/test_events/test_event.py b/pype/ftrack/test_events/test_event.py deleted file mode 100644 index e662fa8011..0000000000 --- a/pype/ftrack/test_events/test_event.py +++ /dev/null @@ -1,16 +0,0 @@ -# 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 deleted file mode 100644 index 7241ebe65d..0000000000 --- a/pype/ftrack/test_events/test_event2.py +++ /dev/null @@ -1,16 +0,0 @@ -# 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 ---------------------------------- From 490bf1c3ffef051b7e814b55d317a3df343c569b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 2 Nov 2018 19:31:16 +0100 Subject: [PATCH 03/16] Action-handler returns full FT obj and added few restored actions --- pype/ftrack/actions/action_asset_delete.py | 131 +++++++ .../actions/action_client_review_sort.py | 21 +- pype/ftrack/actions/action_component_open.py | 119 ++++++ .../actions/action_createCustomAttributes.py | 25 +- .../actions/action_delete_unpublished.py | 96 +++++ .../actions/action_folders_create_lucidity.py | 186 ++++++++++ pype/ftrack/actions/action_killRunningJobs.py | 17 +- pype/ftrack/actions/action_open_folder.py | 15 +- pype/ftrack/actions/action_set_version.py | 125 +++++++ pype/ftrack/actions/action_syncToAvalon.py | 27 +- pype/ftrack/actions/action_test.py | 18 +- pype/ftrack/actions/action_thumbToChildern.py | 22 +- pype/ftrack/actions/action_thumbToParent.py | 24 +- pype/ftrack/actions/batch_create.py | 349 ++++++++++++++++++ pype/ftrack/actions/djvview_launch.py | 115 ++++++ pype/ftrack/actions/ftrack_action_handler.py | 7 +- 16 files changed, 1169 insertions(+), 128 deletions(-) create mode 100644 pype/ftrack/actions/action_asset_delete.py create mode 100644 pype/ftrack/actions/action_component_open.py create mode 100644 pype/ftrack/actions/action_delete_unpublished.py create mode 100644 pype/ftrack/actions/action_folders_create_lucidity.py create mode 100644 pype/ftrack/actions/action_set_version.py create mode 100644 pype/ftrack/actions/batch_create.py create mode 100644 pype/ftrack/actions/djvview_launch.py diff --git a/pype/ftrack/actions/action_asset_delete.py b/pype/ftrack/actions/action_asset_delete.py new file mode 100644 index 0000000000..5c91acfff7 --- /dev/null +++ b/pype/ftrack/actions/action_asset_delete.py @@ -0,0 +1,131 @@ +import sys +import argparse +import logging +import getpass +import ftrack_api +from ftrack_action_handler import BaseAction + + +class AssetDelete(BaseAction): + '''Custom action.''' + + #: Action identifier. + identifier = 'asset.delete' + #: Action label. + label = 'Asset Delete' + + + def discover(self, session, entities, event): + ''' Validation ''' + + if (len(entities) != 1 or entities[0].entity_type + not in ['Shot', 'Asset Build']): + return False + + return True + + + def interface(self, session, entities, event): + + if not event['data'].get('values', {}): + entity = entities[0] + + items = [] + for asset in entity['assets']: + # get asset name for label + label = 'None' + if asset['name']: + label = asset['name'] + + items.append({ + 'label':label, + 'name':label, + 'value':False, + 'type':'boolean' + }) + + if len(items) < 1: + return { + 'success': False, + 'message': 'There are no assets to delete' + } + + return items + + def launch(self, session, entities, event): + + entity = entities[0] + # if values were set remove those items + if 'values' in event['data']: + values = event['data']['values'] + # get list of assets to delete from form + to_delete = [] + for key in values: + if values[key]: + to_delete.append(key) + # delete them by name + for asset in entity['assets']: + if asset['name'] in to_delete: + session.delete(asset) + try: + session.commit() + except: + session.rollback() + raise + + return { + 'success': True, + 'message': 'Asset deleted.' + } + + +def register(session, **kw): + '''Register action. Called when used as an event 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 = AssetDelete(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:])) diff --git a/pype/ftrack/actions/action_client_review_sort.py b/pype/ftrack/actions/action_client_review_sort.py index 4df73178e9..7c364e4505 100644 --- a/pype/ftrack/actions/action_client_review_sort.py +++ b/pype/ftrack/actions/action_client_review_sort.py @@ -19,22 +19,10 @@ class ClientReviewSort(BaseAction): label = 'Sort Review' - def validateSelection(self, entities): - '''Return true if the selection is valid. ''' - - if len(entities) == 0: - return False - - return True - - def discover(self, session, entities, event): - '''Return action config if triggered on a single selection.''' + ''' Validation ''' - selection = event['data']['selection'] - # this action will only handle a single version. - if (not self.validateSelection(entities) or - selection[0]['entityType'] != 'reviewsession'): + if (len(entities) == 0 or entities[0].entity_type != 'ReviewSession'): return False return True @@ -42,8 +30,7 @@ class ClientReviewSort(BaseAction): def launch(self, session, entities, event): - entity_type, entity_id = entities[0] - entity = session.get(entity_type, entity_id) + entity = entities[0] # Get all objects from Review Session and all 'sort order' possibilities obj_list = [] @@ -53,8 +40,8 @@ class ClientReviewSort(BaseAction): sort_order_list.append(obj['sort_order']) # Sort criteria - obj_list = sorted(obj_list, key=lambda k: k['asset_version']['task']['name']) obj_list = sorted(obj_list, key=lambda k: k['version']) + obj_list = sorted(obj_list, key=lambda k: k['asset_version']['task']['name']) obj_list = sorted(obj_list, key=lambda k: k['name']) # Set 'sort order' to sorted list, so they are sorted in Ftrack also diff --git a/pype/ftrack/actions/action_component_open.py b/pype/ftrack/actions/action_component_open.py new file mode 100644 index 0000000000..1c67f4f22f --- /dev/null +++ b/pype/ftrack/actions/action_component_open.py @@ -0,0 +1,119 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2015 Milan Kolar + +import sys +import argparse +import logging +import getpass +import subprocess +import os +import ftrack_api +from ftrack_action_handler import BaseAction + + +class ComponentOpen(BaseAction): + '''Custom action.''' + + # Action identifier + identifier = 'component.open' + # Action label + label = 'Open File' + # Action icon + icon = 'https://cdn4.iconfinder.com/data/icons/rcons-application/32/application_go_run-256.png', + + + def discover(self, session, entities, event): + ''' Validation ''' + + if len(entities) != 1 or entities[0].entity_type != 'Component': + return False + + return True + + + def launch(self, session, entities, event): + + entity = entities[0] + + # Return error if component is on ftrack server + if entity['component_locations'][0]['location']['name'] == 'ftrack.server': + return { + 'success': False, + 'message': "This component is stored on ftrack server!" + } + + # Get component filepath + fpath = entity['component_locations'][0]['resource_identifier'] + + if os.path.isfile(fpath): + if sys.platform == 'win': # windows + subprocess.Popen('explorer "%s"' % fpath) + elif sys.platform == 'darwin': # macOS + subprocess.Popen(['open', fpath]) + else: # linux + try: + subprocess.Popen(['xdg-open', fpath]) + except OSError: + raise OSError('unsupported xdg-open call??') + else: + return { + 'success': False, + 'message': "Didn't found file: " + fpath + } + + return { + 'success': True, + 'message': 'Component Opened' + } + + +def register(session, **kw): + '''Register action. Called when used as an event 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 = ComponentOpen(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:])) diff --git a/pype/ftrack/actions/action_createCustomAttributes.py b/pype/ftrack/actions/action_createCustomAttributes.py index 70085f9e69..bba05e7a77 100644 --- a/pype/ftrack/actions/action_createCustomAttributes.py +++ b/pype/ftrack/actions/action_createCustomAttributes.py @@ -21,27 +21,16 @@ class AvalonIdAttribute(BaseAction): #: Action description. description = 'Creates Avalon/Mongo ID for double check' - 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.''' + ''' Validation ''' - self.logger.info('Got selection: {0}'.format(entities)) - return self.validate_selection(session, entities) + # userId = event['source']['user']['id'] + # user = session.query('User where id is ' + userId).one() + # if user['user_security_roles'][0]['security_role']['name'] != 'Administrator': + # return False + + return True def importToAvalon(self, session, entity): diff --git a/pype/ftrack/actions/action_delete_unpublished.py b/pype/ftrack/actions/action_delete_unpublished.py new file mode 100644 index 0000000000..effa66072e --- /dev/null +++ b/pype/ftrack/actions/action_delete_unpublished.py @@ -0,0 +1,96 @@ +import sys +import argparse +import logging +import getpass +import ftrack_api +from ftrack_action_handler import BaseAction + + +class VersionsCleanup(BaseAction): + '''Custom action.''' + + # Action identifier + identifier = 'versions.cleanup' + # Action label + label = 'Versions cleanup' + + + def discover(self, session, entities, event): + ''' Validation ''' + + # Only 1 AssetVersion is allowed + if len(entities) != 1 or entities[0].entity_type != 'AssetVersion': + return False + + return True + + def launch(self, session, entities, event): + + entity = entities[0] + + # Go through all versions in asset + for version in entity['asset']['versions']: + if not version['is_published']: + session.delete(version) + try: + session.commit() + except: + session.rollback() + raise + + return { + 'success': True, + 'message': 'removed hidden versions' + } + + +def register(session, **kw): + '''Register action. Called when used as an event 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 = VersionsCleanup(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:])) diff --git a/pype/ftrack/actions/action_folders_create_lucidity.py b/pype/ftrack/actions/action_folders_create_lucidity.py new file mode 100644 index 0000000000..857b1fc35e --- /dev/null +++ b/pype/ftrack/actions/action_folders_create_lucidity.py @@ -0,0 +1,186 @@ +import logging +import os +import getpass +import argparse +import errno +import sys +import threading +import ftrack_api +from ftrack_action_handler import BaseAction + +PLUGIN_DIRECTORY = os.path.abspath( + os.path.join(os.path.dirname(__file__), '..')) + +if PLUGIN_DIRECTORY not in sys.path: + sys.path.append(PLUGIN_DIRECTORY) + +import ft_utils + + +def async(fn): + '''Run *fn* asynchronously.''' + def wrapper(*args, **kwargs): + thread = threading.Thread(target=fn, args=args, kwargs=kwargs) + thread.start() + return wrapper + + +class CreateFolders(BaseAction): + + #: Action identifier. + identifier = 'create.folders' + #: Action label. + label = 'Create Folders' + #: Action Icon. + icon = 'https://cdn1.iconfinder.com/data/icons/rcons-folder-action/32/folder_add-512.png' + + raise ValueError('Not working version of action') + @async + def createFoldersFromEntity(self, entity): + '''Generate folder structure from *entity*. + + Entity is assumed to be either a project, episode, sequence or shot. + + ''' + + root = entity.getProject().getRoot() + + self.logger.info(root) + + if entity.getObjectType() in ( + 'Episode', 'Sequence', 'Folder', 'Shot'): + objects = entity.getChildren(objectType='Shot', depth=None) + objects.append(entity) + else: + objects = entity.getChildren(depth=None) + + for obj in objects: + + tasks = obj.getTasks() + paths_collected = set([]) + if obj.getObjectType() in ( + 'Episode', 'Sequence', 'Shot', 'Folder'): + task_mask = 'shot.task' + else: + task_mask = 'asset.task' + + self.logger.info(task_mask) + + for task in tasks: + self.logger.info(task) + paths = ft_utils.getAllPathsYaml(task) + self.logger.info(paths) + for path in paths: + if task_mask in path[1].name: + temppath = os.path.join( + root, path[0].lower().replace(" ", '_').replace('\'', '')) + paths_collected.add(temppath) + + for path in paths_collected: + self.logger.info(path) + try: + os.makedirs(path) + except OSError as error: + if error.errno != errno.EEXIST: + raise + + + def discover(self, session, entities, event): + + if len(entities) == 0 or entities[0].entity_type not in [ + 'Episode', 'Sequence', 'Shot', 'Folder', 'Asset Build']: + return False + + return True + + + def launch(self, session, entities, event): + + 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': 'Creating Folders' + }) + }) + + '''Callback method for custom action.''' + + try: + session.event_hub.publishReply( + event, + data={ + 'success': True, + 'message': 'Folder Creation Job Started!' + } + ) + + for entity in entities: + self.createFoldersFromEntity(entity) + + job.setStatus('done') + except: + job.setStatus('failed') + raise + + + return { + 'success': True, + 'message': 'Created Folders Successfully!' + } + + +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 = CreateFolders(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:])) diff --git a/pype/ftrack/actions/action_killRunningJobs.py b/pype/ftrack/actions/action_killRunningJobs.py index 450788df0e..da8ad30e33 100644 --- a/pype/ftrack/actions/action_killRunningJobs.py +++ b/pype/ftrack/actions/action_killRunningJobs.py @@ -20,20 +20,14 @@ class JobKiller(BaseAction): description = 'Killing all running jobs younger than day' - def validate_selection(self, session, entities): - '''Return if *entities* is a valid selection.''' - pass + def discover(self, session, entities, event): + ''' Validation ''' 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): - """ JOB SETTING """ + """ GET JOB """ yesterday = datetime.date.today() - datetime.timedelta(days=1) @@ -48,7 +42,10 @@ class JobKiller(BaseAction): print('Changing Job ({}) status: {} -> failed'.format(job['id'], job['status'])) job['status'] = 'failed' - session.commit() + try: + session.commit() + except: + session.rollback() print('All running jobs were killed Successfully!') return { diff --git a/pype/ftrack/actions/action_open_folder.py b/pype/ftrack/actions/action_open_folder.py index b301d36ae7..6c582adf2a 100644 --- a/pype/ftrack/actions/action_open_folder.py +++ b/pype/ftrack/actions/action_open_folder.py @@ -18,21 +18,16 @@ class openFolder(BaseAction): label = 'Open Folders' #: Action Icon. icon = "https://cdn3.iconfinder.com/data/icons/stroke/53/Open-Folder-256.png" + raise ValueError('Not working version of action') + def discover(self, session, entities, event): + ''' Validation ''' - def validateSelection(self, selection): - '''Return true if the selection is valid. ''' - - if len(selection) == 0 or selection[0]['entityType'] in ['assetversion', 'Component']: + if len(entities) == 0 or entities[0].entity_type in ['assetversion', 'Component']: return False return True - def discover(self, session, entities, event): - selection = event['data']['selection'] - - # validate selection, and only return action if it is valid. - return self.validateSelection(selection) def get_paths(self, entity): '''Prepare all the paths for the entity. @@ -72,8 +67,6 @@ class openFolder(BaseAction): hits = set([]) for entity in entities: - entity_type, entity_id = entity - entity = session.get(entity_type, entity_id) # Get paths base on the entity. # This function needs to be chagned to fit your path logic diff --git a/pype/ftrack/actions/action_set_version.py b/pype/ftrack/actions/action_set_version.py new file mode 100644 index 0000000000..416f4db960 --- /dev/null +++ b/pype/ftrack/actions/action_set_version.py @@ -0,0 +1,125 @@ +import sys +import argparse +import logging +import getpass +import ftrack_api +from ftrack_action_handler import BaseAction + + +class SetVersion(BaseAction): + '''Custom action.''' + + #: Action identifier. + identifier = 'version.set' + + #: Action label. + label = 'Version Set' + + + def discover(self, session, entities, event): + ''' Validation ''' + + # Only 1 AssetVersion is allowed + if len(entities) != 1 or entities[0].entity_type != 'AssetVersion': + return False + + return True + + def interface(self, session, entities, event): + + if not event['data'].get('values', {}): + entity = entities[0] + + # Get actual version of asset + act_ver = entity['version'] + # Set form + items = [{ + 'label': 'Version number', + 'type': 'number', + 'name': 'version_number', + 'value': act_ver + }] + + return items + + def launch(self, session, entities, event): + + entity = entities[0] + + # Do something with the values or return a new form. + values = event['data'].get('values', {}) + # Default is action True + scs = True + msg = 'Version was changed to v{0}'.format(values['version_number']) + + if not values['version_number']: + scs = False, + msg = "You didn't enter any version." + elif int(values['version_number']) <= 0: + scs = False + msg = 'Negative or zero version is not valid.' + else: + entity['version'] = values['version_number'] + + try: + session.commit() + except: + session.rollback() + raise + + return { + 'success': scs, + 'message': msg + } + + +def register(session, **kw): + '''Register action. Called when used as an event 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 = SetVersion(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:])) diff --git a/pype/ftrack/actions/action_syncToAvalon.py b/pype/ftrack/actions/action_syncToAvalon.py index ed65396f37..a557a169ab 100644 --- a/pype/ftrack/actions/action_syncToAvalon.py +++ b/pype/ftrack/actions/action_syncToAvalon.py @@ -23,27 +23,11 @@ class SyncToAvalon(BaseAction): #: 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.''' + ''' Validation ''' - self.logger.info('Got selection: {0}'.format(entities)) - return self.validate_selection(session, entities) + return True def importToAvalon(self, session, entity): @@ -51,6 +35,7 @@ class SyncToAvalon(BaseAction): 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: @@ -198,17 +183,15 @@ class SyncToAvalon(BaseAction): # get all entities separately/unique for entity in entities: - entity_type, entity_id = entity - act_ent = session.get(entity_type, entity_id) - getShotAsset(act_ent) + getShotAsset(entity) 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!') diff --git a/pype/ftrack/actions/action_test.py b/pype/ftrack/actions/action_test.py index aae44aad44..08d7704825 100644 --- a/pype/ftrack/actions/action_test.py +++ b/pype/ftrack/actions/action_test.py @@ -18,32 +18,22 @@ class TestAction(BaseAction): #: 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.''' + + def discover(self, session, entities, event): + ''' Validation ''' 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): for entity in entities: - entity_type, entity_id = entity - entity = session.get(entity_type, entity_id) - - import ft_utils - print(ft_utils.getNewContext(entity)) + print("TEST") return True diff --git a/pype/ftrack/actions/action_thumbToChildern.py b/pype/ftrack/actions/action_thumbToChildern.py index 8a2889aae0..54974c22d6 100644 --- a/pype/ftrack/actions/action_thumbToChildern.py +++ b/pype/ftrack/actions/action_thumbToChildern.py @@ -20,23 +20,14 @@ class ThumbToChildren(BaseAction): # Action icon icon = "https://cdn3.iconfinder.com/data/icons/transfers/100/239322-download_transfer-128.png" - def validateSelection(self, selection): - '''Return true if the selection is valid. - - Legacy plugins can only be started from a single Task. ''' - - if len(selection) > 0: - if selection[0]['entityType'] in ['assetversion', 'task']: - return True - - return False def discover(self, session, entities, event): - '''Return action config if triggered on asset versions.''' - selection = event['data']['selection'] + ''' Validation ''' - # validate selection, and only return action if it is valid. - return self.validateSelection(selection) + if (len(entities) <= 0 or entities[0].entity_type in ['Project']): + return False + + return True def launch(self, session, entities, event): @@ -55,9 +46,6 @@ class ThumbToChildren(BaseAction): try: for entity in entities: - entity_type, entity_id = entity - entity = session.get(entity_type, entity_id) - thumbid = entity['thumbnail_id'] if thumbid: for child in entity['children']: diff --git a/pype/ftrack/actions/action_thumbToParent.py b/pype/ftrack/actions/action_thumbToParent.py index 08c6c24674..82954ae0e5 100644 --- a/pype/ftrack/actions/action_thumbToParent.py +++ b/pype/ftrack/actions/action_thumbToParent.py @@ -20,23 +20,13 @@ class ThumbToParent(BaseAction): icon = "https://cdn3.iconfinder.com/data/icons/transfers/100/239419-upload_transfer-512.png" - def validateSelection(self, selection): - '''Return true if the selection is valid. - - Legacy plugins can only be started from a single Task. - - ''' - if len(selection) > 0: - if selection[0]['entityType'] in ['assetversion', 'task']: - return True - - return False - def discover(self, session, entities, event): '''Return action config if triggered on asset versions.''' - selection = event['data']['selection'] - # validate selection, and only return action if it is valid. - return self.validateSelection(selection) + + if len(entities) <= 0 or entities[0].entity_type in ['Project']: + return False + + return True def launch(self, session, entities, event): @@ -55,9 +45,6 @@ class ThumbToParent(BaseAction): try: for entity in entities: - entity_type, entity_id = entity - entity = session.get(entity_type, entity_id) - if entity.entity_type.lower() == 'assetversion': try: parent = entity['task'] @@ -76,6 +63,7 @@ class ThumbToParent(BaseAction): # inform the user that the job is done job['status'] = 'done' session.commit() + except: # fail the job if something goes wrong job['status'] = 'failed' diff --git a/pype/ftrack/actions/batch_create.py b/pype/ftrack/actions/batch_create.py new file mode 100644 index 0000000000..c17596367c --- /dev/null +++ b/pype/ftrack/actions/batch_create.py @@ -0,0 +1,349 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2015 ftrack +import sys +import argparse +import logging +import collections +import threading +import getpass +import ftrack_api +from ftrack_action_handler import BaseAction + +STRUCTURE_NAMES = ['episode', 'sequence', 'shot'] + + +TASK_TYPE_ENUMERATOR_OPTIONS = [ + {'label': task_type.getName(), 'value': task_type.getId()} + for task_type in ftrack.getTaskTypes() +] + +TASK_TYPE_LOOKUP = dict( + (task_type.getId(), task_type.getName()) + for task_type in ftrack.getTaskTypes() +) + + +def async(fn): + '''Run *fn* asynchronously.''' + def wrapper(*args, **kwargs): + thread = threading.Thread(target=fn, args=args, kwargs=kwargs) + thread.start() + return wrapper + + +def get_names(base_name, padding, start, end, incremental): + '''Return names from expression.''' + names = [] + for part in range(start, end + incremental, incremental): + names.append( + base_name + str(part).zfill(padding) + ) + return names + + +def generate_structure(values): + '''Return structure from *values*.''' + structure = [] + + for structure_name in STRUCTURE_NAMES: + if (structure_name + '_expression') not in values: + continue + + object_expression = values[structure_name + '_expression'] + object_incremental = values[structure_name + '_incremental'] + + padding = object_expression.count('#') + _range, incremental = object_incremental.split(':') + start, end = _range.split('-') + + start = int(start) + end = int(end) + incremental = int(incremental) + + base_name = object_expression.replace('#', '') + + logging.info( + ( + 'Create from expression {expression} with {base_name}, ' + '{padding} and {start}-{end}:{incremental}' + ).format( + expression=object_expression, + base_name=base_name, + padding=padding, + start=start, + end=end, + incremental=incremental + ) + ) + + names = get_names( + base_name=base_name, + padding=padding, + start=start, + end=end, + incremental=incremental + ) + + structure.append({ + 'object_type': structure_name, + 'data': names + }) + + tasks = collections.defaultdict(dict) + for name, value in values.iteritems(): + if name.startswith('task_'): + _, index, key = name.split('_') + if key == 'bid' and value: + value = float(value) * 3600 + tasks[index][key] = value + + task_data = [] + structure.append({ + 'object_type': 'task', + 'data': task_data + }) + for task in tasks.values(): + task_data.append(task) + + return structure + + +@async +def create(parent, structure): + '''Create *structure* under *parent*.''' + return create_from_structure(parent, structure) + + +def create_from_structure(parent, structure): + '''Create *structure* under *parent*.''' + level = structure[0] + children = structure[1:] + object_type = level['object_type'] + + for data in level['data']: + + if object_type == 'episode': + new_object = parent.createEpisode(data) + + if object_type == 'sequence': + new_object = parent.createSequence(data) + + if object_type == 'shot': + new_object = parent.createShot(data) + + if object_type == 'task': + new_object = parent.createTask( + TASK_TYPE_LOOKUP[data['typeid']] + ) + new_object.set(data) + + logging.info( + 'Created {new_object} on parent {parent}'.format( + parent=parent, new_object=new_object + ) + ) + if children: + create_from_structure(new_object, children) + + +def get_form(number_of_tasks, structure_type, prefix, padding_count): + '''Return form from *number_of_tasks* and *structure_type*.''' + mappings = { + 'episode': ['episode', 'sequence', 'shot'], + 'sequence': ['sequence', 'shot'], + 'shot': ['shot'] + } + + items = [] + + for structure_name in mappings[structure_type]: + items.extend( + [ + { + 'value': '##{0}##'.format(structure_name.capitalize()), + 'type': 'label' + }, { + 'label': 'Expression', + 'type': 'text', + 'value': prefix + '#' * padding_count, + 'name': '{0}_expression'.format(structure_name) + }, { + 'label': 'Incremental', + 'type': 'text', + 'value': '10-20:10', + 'name': '{0}_incremental'.format(structure_name) + } + ] + ) + + for index in range(0, number_of_tasks): + items.extend( + [ + { + 'value': '##Template for Task{0}##'.format(index), + 'type': 'label' + }, + { + 'label': 'Type', + 'type': 'enumerator', + 'name': 'task_{0}_typeid'.format(index), + 'data': TASK_TYPE_ENUMERATOR_OPTIONS + }, + { + 'label': 'Bid', + 'type': 'number', + 'name': 'task_{0}_bid'.format(index) + } + ] + ) + + return {'items': items} + + +class BatchCreate(BaseAction): + '''Batch create objects in ftrack.''' + + #: Action identifier. + identifier = 'batch_create' + #: Action label. + label = 'Batch create' + + def discover(self, session, entities, event): + + if (len(entities) != 1 or entities[0].entity_type.lower() + not in ['project', 'episode', 'sequence']): + return False + + return True + + def interface(self, session, entities, event): + if 'values' not in event['data']: + data = [ + { + 'label': 'Episode, Sequence, Shot', + 'value': 'episode' + }, { + 'label': 'Sequence, Shot', + 'value': 'sequence' + }, { + 'label': 'Shot', + 'value': 'shot' + } + ] + entity = None + data_value = 'episode' + entity_name = '' + try: + entity = ftrack.Project(selection[0]['entityId']) + entity_name = entity.getFullName() + except: + pass + try: + entity = ftrack.Task(selection[0]['entityId']) + object_type = entity.getObjectType() + entity_name = entity.getName() + + if object_type == 'Episode': + del data[0] + data_value = 'sequence' + + if object_type == 'Sequence': + del data[0] + del data[0] + data_value = 'shot' + except: + pass + return [ + { + 'label': 'Select structure', + 'type': 'enumerator', + 'value': data_value, + 'name': 'structure_type', + 'data': data + }, { + 'label': 'Padding count', + 'type': 'number', + 'name': 'padding_count', + 'value': 4 + }, { + 'label': 'Number of tasks', + 'type': 'number', + 'name': 'number_of_tasks', + 'value': 2 + } + ] + + def launch(self, session, entities, event): + '''Callback method for action.''' + selection = event['data'].get('selection', []) + values = event['data'].get('values', {}) + if values: + if 'number_of_tasks' in values: + form = get_form( + int(values['number_of_tasks']), + values['structure_type'], + entity_name + '_', + int(values['padding_count']) + ) + return form + + else: + structure = generate_structure(values) + logging.info('Creating structure "{0}"'.format(str(structure))) + create(entity, structure) + return { + 'success': True, + 'message': 'Action completed successfully' + } + + +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 = BatchCreate(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:])) diff --git a/pype/ftrack/actions/djvview_launch.py b/pype/ftrack/actions/djvview_launch.py new file mode 100644 index 0000000000..6b3e491cff --- /dev/null +++ b/pype/ftrack/actions/djvview_launch.py @@ -0,0 +1,115 @@ +import os +import logging +import json + +import ftrack +import ftrack_api +import clique +import ftrack_template + +log = logging.getLogger(__name__) + + +def modify_launch(session, event): + """Modify the application launch command with potential files to open""" + + # Collect published paths + data = {} + for item in event["data"].get("selection", []): + + versions = [] + + if entity.entity_type == "Assetversion": + version = ftrack.AssetVersion(item["entityId"]) + if version.getAsset().getType().getShort() in ["img", "mov"]: + versions.append(version) + + # Add latest version of "img" and "mov" type from tasks. + if item["entityType"] == "task": + task = ftrack.Task(item["entityId"]) + for asset in task.getAssets(assetTypes=["img", "mov"]): + versions.append(asset.getVersions()[-1]) + + for version in versions: + for component in version.getComponents(): + component_list = data.get(component.getName(), []) + component_list.append(component) + data[component.getName()] = component_list + + label = "v{0} - {1} - {2}" + label = label.format( + str(version.getVersion()).zfill(3), + version.getAsset().getType().getName(), + component.getName() + ) + + file_path = component.getFilesystemPath() + if component.isSequence(): + if component.getMembers(): + frame = int(component.getMembers()[0].getName()) + file_path = file_path % frame + + event["data"]["items"].append( + {"label": label, "value": file_path} + ) + + # Collect workspace paths + session = ftrack_api.Session() + for item in event["data"].get("selection", []): + if item["entityType"] == "task": + templates = ftrack_template.discover_templates() + task_area, template = ftrack_template.format( + {}, templates, entity=session.get("Task", item["entityId"]) + ) + + # Traverse directory and collect collections from json files. + instances = [] + for root, dirs, files in os.walk(task_area): + for f in files: + if f.endswith(".json"): + with open(os.path.join(root, f)) as json_data: + for data in json.load(json_data): + instances.append(data) + + check_values = [] + for data in instances: + if "collection" in data: + + # Check all files in the collection + collection = clique.parse(data["collection"]) + for f in list(collection): + if not os.path.exists(f): + collection.remove(f) + + if list(collection): + value = list(collection)[0] + + # Check if value already exists + if value in check_values: + continue + else: + check_values.append(value) + + # Add workspace items + event["data"]["items"].append( + { + "label": "{0} - {1}".format( + data["name"], + os.path.basename(collection.format()) + ), + "value": value + } + ) + + return event + + +def register(session, **kw): + # Validate session + if not isinstance(session, ftrack_api.session.Session): + return + + session.event_hub.subscribe( + 'topic=djvview.launch', + modify_launch(session) + ) diff --git a/pype/ftrack/actions/ftrack_action_handler.py b/pype/ftrack/actions/ftrack_action_handler.py index 80fbaabb5d..d1e2ea58df 100644 --- a/pype/ftrack/actions/ftrack_action_handler.py +++ b/pype/ftrack/actions/ftrack_action_handler.py @@ -83,6 +83,7 @@ class AppAction(object): ) if accepts: + self.logger.info('Selection is valid') return { 'items': [{ 'label': self.label, @@ -92,6 +93,8 @@ class AppAction(object): 'icon': self.icon, }] } + else: + self.logger.info('Selection is _not_ valid') def discover(self, session, entities, event): '''Return true if we can handle the selected entities. @@ -425,6 +428,7 @@ class BaseAction(object): ) if accepts: + self.logger.info(u'Discovering action with selection: {0}'.format(args[1]['data'].get('selection', []))) return { 'items': [{ 'label': self.label, @@ -462,7 +466,8 @@ class BaseAction(object): for entity in _selection: _entities.append( ( - self._get_entity_type(entity), entity.get('entityId') + session.get(self._get_entity_type(entity), entity.get('entityId')) + # self._get_entity_type(entity), entity.get('entityId') ) ) From 94515dea6827d85632b156a2869d0be1304be33c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 6 Nov 2018 17:27:31 +0100 Subject: [PATCH 04/16] "Component open" and "DJV launcher" actions are working. NOT PROPERLY!!! --- pype/ftrack/actions/action_component_open.py | 13 +- pype/ftrack/actions/action_syncToAvalon.py | 5 +- pype/ftrack/actions/action_test.py | 17 +- pype/ftrack/actions/djvview.py | 383 +++++++++++++++++++ pype/ftrack/actions/djvview_launch.py | 115 ------ 5 files changed, 408 insertions(+), 125 deletions(-) create mode 100644 pype/ftrack/actions/djvview.py delete mode 100644 pype/ftrack/actions/djvview_launch.py diff --git a/pype/ftrack/actions/action_component_open.py b/pype/ftrack/actions/action_component_open.py index 1c67f4f22f..34ee752f61 100644 --- a/pype/ftrack/actions/action_component_open.py +++ b/pype/ftrack/actions/action_component_open.py @@ -24,8 +24,7 @@ class ComponentOpen(BaseAction): def discover(self, session, entities, event): ''' Validation ''' - - if len(entities) != 1 or entities[0].entity_type != 'Component': + if len(entities) != 1 or entities[0].entity_type != 'FileComponent': return False return True @@ -43,10 +42,14 @@ class ComponentOpen(BaseAction): } # Get component filepath + # TODO with locations it will be different??? fpath = entity['component_locations'][0]['resource_identifier'] + items = fpath.split(os.sep) + items.pop(-1) + fpath = os.sep.join(items) - if os.path.isfile(fpath): - if sys.platform == 'win': # windows + if os.path.isdir(fpath): + if 'win' in sys.platform: # windows subprocess.Popen('explorer "%s"' % fpath) elif sys.platform == 'darwin': # macOS subprocess.Popen(['open', fpath]) @@ -63,7 +66,7 @@ class ComponentOpen(BaseAction): return { 'success': True, - 'message': 'Component Opened' + 'message': 'Component folder Opened' } diff --git a/pype/ftrack/actions/action_syncToAvalon.py b/pype/ftrack/actions/action_syncToAvalon.py index a557a169ab..3808f5c8ff 100644 --- a/pype/ftrack/actions/action_syncToAvalon.py +++ b/pype/ftrack/actions/action_syncToAvalon.py @@ -4,7 +4,6 @@ import sys import argparse import logging import os -import json import ftrack_api from ftrack_action_handler import BaseAction @@ -161,9 +160,7 @@ class SyncToAvalon(BaseAction): job = session.create('Job', { 'user': user, 'status': 'running', - 'data': json.dumps({ - 'description': 'Synch Ftrack to Avalon.' - }) + 'data': {'description': 'Synch Ftrack to Avalon.'} }) try: diff --git a/pype/ftrack/actions/action_test.py b/pype/ftrack/actions/action_test.py index 08d7704825..a9f2aba0eb 100644 --- a/pype/ftrack/actions/action_test.py +++ b/pype/ftrack/actions/action_test.py @@ -33,7 +33,22 @@ class TestAction(BaseAction): def launch(self, session, entities, event): for entity in entities: - print("TEST") + index = 0 + name = entity['components'][index]['name'] + filetype = entity['components'][index]['file_type'] + path = entity['components'][index]['component_locations'][0]['resource_identifier'] + + # entity['components'][index]['component_locations'][0]['resource_identifier'] = r"C:\Users\jakub.trllo\Desktop\test\exr\int_c022_lighting_v001_main_AO.%04d.exr" + location = entity['components'][0]['component_locations'][0]['location'] + component = entity['components'][0] + + + # print(location.get_filesystem_path(component)) + + # for k in p: + # print(100*"-") + # print(k) + # print(p[k]) return True diff --git a/pype/ftrack/actions/djvview.py b/pype/ftrack/actions/djvview.py new file mode 100644 index 0000000000..87213c1308 --- /dev/null +++ b/pype/ftrack/actions/djvview.py @@ -0,0 +1,383 @@ +import logging +import subprocess +import sys +import pprint +import os +import getpass +import re +from operator import itemgetter +import ftrack_api + + +class DJVViewAction(object): + """Launch DJVView action.""" + identifier = "djvview-launch-action" + # label = "DJV View" + # icon = "http://a.fsdn.com/allura/p/djv/icon" + + def __init__(self, session): + '''Expects a ftrack_api.Session instance''' + + self.logger = logging.getLogger( + '{0}.{1}'.format(__name__, self.__class__.__name__) + ) + + if self.identifier is None: + raise ValueError( + 'Action missing identifier.' + ) + + self.session = session + + def is_valid_selection(self, event): + selection = event["data"].get("selection", []) + + if not selection: + return + + entityType = selection[0]["entityType"] + + if entityType not in ["assetversion", "task"]: + return False + + return True + + def discover(self, event): + """Return available actions based on *event*. """ + + if not self.is_valid_selection(event): + return + + items = [] + applications = self.get_applications() + applications = sorted( + applications, key=lambda application: application["label"] + ) + + for application in applications: + self.djv_path = application.get("path", None) + applicationIdentifier = application["identifier"] + label = application["label"] + items.append({ + "actionIdentifier": self.identifier, + "label": label, + "variant": application.get("variant", None), + "description": application.get("description", None), + "icon": application.get("icon", "default"), + "applicationIdentifier": applicationIdentifier + }) + + return { + "items": items + } + + def register(self): + '''Registers the action, subscribing the 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 + ) + + 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 + ), + self.launch + ) + print("----- action - <" + self.__class__.__name__ + "> - Has been registered -----") + + def get_applications(self): + applications = [] + + label="DJVView {version}" + versionExpression=re.compile(r"(?P\d+.\d+.\d+)") + applicationIdentifier="djvview" + description="DJV View Launcher" + icon="http://a.fsdn.com/allura/p/djv/icon" + expression = [] + if sys.platform == "win32": + expression = ["C:\\", "Program Files", "djv-\d.+", + "bin", "djv_view.exe"] + + elif sys.platform == "darwin": + expression = ["Application", "DJV.app", "Contents", "MacOS", "DJV"] + ## Linuxs + else: + expression = ["usr", "local", "djv", "djv_view"] + + pieces = expression[:] + start = pieces.pop(0) + + if sys.platform == 'win32': + # On Windows C: means current directory so convert roots that look + # like drive letters to the C:\ format. + if start and start[-1] == ':': + start += '\\' + + if not os.path.exists(start): + raise ValueError( + 'First part "{0}" of expression "{1}" must match exactly to an ' + 'existing entry on the filesystem.' + .format(start, expression) + ) + + + expressions = list(map(re.compile, pieces)) + expressionsCount = len(expression)-1 + + for location, folders, files in os.walk(start, topdown=True, followlinks=True): + level = location.rstrip(os.path.sep).count(os.path.sep) + expression = expressions[level] + + if level < (expressionsCount - 1): + # If not yet at final piece then just prune directories. + folders[:] = [folder for folder in folders + if expression.match(folder)] + else: + # Match executable. Note that on OSX executable might equate to + # a folder (.app). + for entry in folders + files: + match = expression.match(entry) + if match: + # Extract version from full matching path. + path = os.path.join(start, location, entry) + versionMatch = versionExpression.search(path) + if versionMatch: + version = versionMatch.group('version') + + applications.append({ + 'identifier': applicationIdentifier.format( + version=version + ), + 'path': path, + 'version': version, + 'label': label.format(version=version), + 'icon': icon, + # 'variant': variant.format(version=version), + 'description': description + }) + else: + self.logger.debug( + 'Discovered application executable, but it ' + 'does not appear to o contain required version ' + 'information: {0}'.format(path) + ) + + # Don't descend any further as out of patterns to match. + del folders[:] + + return applications + + def translate_event(self, session, event): + '''Return *event* translated structure to be used with the API.''' + + selection = event['data'].get('selection', []) + + entities = list() + for entity in selection: + entities.append( + (session.get(self.get_entity_type(entity), entity.get('entityId'))) + ) + + return entities + + def get_entity_type(self, entity): + entity_type = entity.get('entityType').replace('_', '').lower() + + for schema in self.session.schemas: + alias_for = schema.get('alias_for') + + if ( + alias_for and isinstance(alias_for, str) and + alias_for.lower() == entity_type + ): + return schema['id'] + + for schema in self.session.schemas: + if schema['id'].lower() == entity_type: + return schema['id'] + + raise ValueError( + 'Unable to translate entity type: {0}.'.format(entity_type) + ) + + def launch(self, event): + """Callback method for DJVView action.""" + session = self.session + entities = self.translate_event(session, event) + + # Launching application + if "values" in event["data"]: + + filename = event['data']['values']['path'] + + # TODO These should be obtained in another way + start = 375 + end = 379 + fps = 24 + # TODO issequence is probably already built-in validation in ftrack + isseq = re.findall( '%[0-9]*d', filename ) + if len(isseq) > 0: + padding = re.findall( '%[0-9]*d', filename ).pop() + range = ( padding % start ) + '-' + ( padding % end ) + filename = re.sub( '%[0-9]*d', range, filename ) + + + cmd = [] + # DJV path + cmd.append( os.path.normpath( self.djv_path ) ) + ### DJV Options Start ################################################ + # cmd.append( '-file_layer (value)' ) #layer name + cmd.append( '-file_proxy 1/2' ) #Proxy scale: 1/2, 1/4, 1/8 + cmd.append( '-file_cache True' ) # Cache: True, False. + # cmd.append( '-window_fullscreen' ) #Start in full screen + # cmd.append("-window_toolbar False") # Toolbar controls: False, True. + # cmd.append("-window_playbar False") # Window controls: False, True. + # cmd.append("-view_grid None") # Grid overlay: None, 1x1, 10x10, 100x100. + # cmd.append("-view_hud True") # Heads up display: True, False. + cmd.append("-playback Forward") # Playback: Stop, Forward, Reverse. + # cmd.append("-playback_frame (value)") # Frame. + cmd.append("-playback_speed " + str(fps)) + # cmd.append("-playback_timer (value)") # Timer: Sleep, Timeout. Value: Sleep. + # cmd.append("-playback_timer_resolution (value)") # Timer resolution (seconds): 0.001. + cmd.append("-time_units Frames") # Time units: Timecode, Frames. + ### DJV Options End ################################################## + + # PATH TO COMPONENT + cmd.append( os.path.normpath( filename ) ) + + # Run DJV with these commands + subprocess.Popen( ' '.join( cmd ) ) + + return { + 'success': True, + 'message': 'DJV View started.' + } + + if 'items' not in event["data"]: + event["data"]['items'] = [] + + try: + for entity in entities: + versions = [] + allowed_types = ["img", "mov", "exr"] + + if entity.entity_type.lower() == "assetversion": + if entity['components'][0]['file_type'] in allowed_types: + versions.append(entity) + + if entity.entity_type.lower() == "task": + # AssetVersions are obtainable only from shot! + shotentity = entity['parent'] + + for asset in shotentity['assets']: + for version in asset['versions']: + # Get only AssetVersion of selected task + if version['task']['id'] != entity['id']: + continue + # Get only components with allowed type + if version['components'][0]['file_type'] in allowed_types: + versions.append(version) + + # Raise error if no components were found + if len(versions) < 1: + raise ValueError('There are no Asset Versions to open.') + + for version in versions: + for component in version['components']: + label = "v{0} - {1} - {2}" + + label = label.format( + str(version['version']).zfill(3), + version['asset']['type']['name'], + component['name'] + ) + + try: + # TODO This is proper way to get filepath!!! + # NOT WORKING RIGHT NOW + location = component['component_locations'][0]['location'] + file_path = location.get_filesystem_path(component) + # if component.isSequence(): + # if component.getMembers(): + # frame = int(component.getMembers()[0].getName()) + # file_path = file_path % frame + except: + # This is NOT proper way + file_path = component['component_locations'][0]['resource_identifier'] + + event["data"]["items"].append( + {"label": label, "value": file_path} + ) + + except Exception as e: + return { + 'success': False, + 'message': str(e) + } + + return { + "items": [ + { + "label": "Items to view", + "type": "enumerator", + "name": "path", + "data": sorted( + event["data"]['items'], + key=itemgetter("label"), + reverse=True + ) + } + ] + } + + + + +def register(session, **kw): + """Register hooks.""" + if not isinstance(session, ftrack_api.session.Session): + return + + action = DJVViewAction(session) + action.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:])) diff --git a/pype/ftrack/actions/djvview_launch.py b/pype/ftrack/actions/djvview_launch.py deleted file mode 100644 index 6b3e491cff..0000000000 --- a/pype/ftrack/actions/djvview_launch.py +++ /dev/null @@ -1,115 +0,0 @@ -import os -import logging -import json - -import ftrack -import ftrack_api -import clique -import ftrack_template - -log = logging.getLogger(__name__) - - -def modify_launch(session, event): - """Modify the application launch command with potential files to open""" - - # Collect published paths - data = {} - for item in event["data"].get("selection", []): - - versions = [] - - if entity.entity_type == "Assetversion": - version = ftrack.AssetVersion(item["entityId"]) - if version.getAsset().getType().getShort() in ["img", "mov"]: - versions.append(version) - - # Add latest version of "img" and "mov" type from tasks. - if item["entityType"] == "task": - task = ftrack.Task(item["entityId"]) - for asset in task.getAssets(assetTypes=["img", "mov"]): - versions.append(asset.getVersions()[-1]) - - for version in versions: - for component in version.getComponents(): - component_list = data.get(component.getName(), []) - component_list.append(component) - data[component.getName()] = component_list - - label = "v{0} - {1} - {2}" - label = label.format( - str(version.getVersion()).zfill(3), - version.getAsset().getType().getName(), - component.getName() - ) - - file_path = component.getFilesystemPath() - if component.isSequence(): - if component.getMembers(): - frame = int(component.getMembers()[0].getName()) - file_path = file_path % frame - - event["data"]["items"].append( - {"label": label, "value": file_path} - ) - - # Collect workspace paths - session = ftrack_api.Session() - for item in event["data"].get("selection", []): - if item["entityType"] == "task": - templates = ftrack_template.discover_templates() - task_area, template = ftrack_template.format( - {}, templates, entity=session.get("Task", item["entityId"]) - ) - - # Traverse directory and collect collections from json files. - instances = [] - for root, dirs, files in os.walk(task_area): - for f in files: - if f.endswith(".json"): - with open(os.path.join(root, f)) as json_data: - for data in json.load(json_data): - instances.append(data) - - check_values = [] - for data in instances: - if "collection" in data: - - # Check all files in the collection - collection = clique.parse(data["collection"]) - for f in list(collection): - if not os.path.exists(f): - collection.remove(f) - - if list(collection): - value = list(collection)[0] - - # Check if value already exists - if value in check_values: - continue - else: - check_values.append(value) - - # Add workspace items - event["data"]["items"].append( - { - "label": "{0} - {1}".format( - data["name"], - os.path.basename(collection.format()) - ), - "value": value - } - ) - - return event - - -def register(session, **kw): - # Validate session - if not isinstance(session, ftrack_api.session.Session): - return - - session.event_hub.subscribe( - 'topic=djvview.launch', - modify_launch(session) - ) From c38001016ee46bb6ca33264f56f7142d3da9896e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 6 Nov 2018 18:55:22 +0100 Subject: [PATCH 05/16] Removed unfinished actions (create folder, open folder, batch create) --- .../actions/action_client_review_sort.py | 1 - .../actions/action_createCustomAttributes.py | 1 + .../actions/action_folders_create_lucidity.py | 186 ---------- pype/ftrack/actions/action_open_folder.py | 163 -------- pype/ftrack/actions/batch_create.py | 349 ------------------ pype/ftrack/actions/djvview.py | 56 ++- pype/ftrack/actions/ftrack_action_handler.py | 30 +- pype/ftrack/create_custAttributes_AvalonId.py | 6 +- pype/ftrack/ftrack_utils.py | 8 +- 9 files changed, 46 insertions(+), 754 deletions(-) delete mode 100644 pype/ftrack/actions/action_folders_create_lucidity.py delete mode 100644 pype/ftrack/actions/action_open_folder.py delete mode 100644 pype/ftrack/actions/batch_create.py diff --git a/pype/ftrack/actions/action_client_review_sort.py b/pype/ftrack/actions/action_client_review_sort.py index 7c364e4505..f903239962 100644 --- a/pype/ftrack/actions/action_client_review_sort.py +++ b/pype/ftrack/actions/action_client_review_sort.py @@ -8,7 +8,6 @@ import ftrack_api from ftrack_action_handler import BaseAction - class ClientReviewSort(BaseAction): '''Custom action.''' diff --git a/pype/ftrack/actions/action_createCustomAttributes.py b/pype/ftrack/actions/action_createCustomAttributes.py index bba05e7a77..afcb0a812d 100644 --- a/pype/ftrack/actions/action_createCustomAttributes.py +++ b/pype/ftrack/actions/action_createCustomAttributes.py @@ -11,6 +11,7 @@ from ftrack_action_handler import BaseAction from avalon import io, inventory, lib from avalon.vendor import toml + class AvalonIdAttribute(BaseAction): '''Edit meta data action.''' diff --git a/pype/ftrack/actions/action_folders_create_lucidity.py b/pype/ftrack/actions/action_folders_create_lucidity.py deleted file mode 100644 index 857b1fc35e..0000000000 --- a/pype/ftrack/actions/action_folders_create_lucidity.py +++ /dev/null @@ -1,186 +0,0 @@ -import logging -import os -import getpass -import argparse -import errno -import sys -import threading -import ftrack_api -from ftrack_action_handler import BaseAction - -PLUGIN_DIRECTORY = os.path.abspath( - os.path.join(os.path.dirname(__file__), '..')) - -if PLUGIN_DIRECTORY not in sys.path: - sys.path.append(PLUGIN_DIRECTORY) - -import ft_utils - - -def async(fn): - '''Run *fn* asynchronously.''' - def wrapper(*args, **kwargs): - thread = threading.Thread(target=fn, args=args, kwargs=kwargs) - thread.start() - return wrapper - - -class CreateFolders(BaseAction): - - #: Action identifier. - identifier = 'create.folders' - #: Action label. - label = 'Create Folders' - #: Action Icon. - icon = 'https://cdn1.iconfinder.com/data/icons/rcons-folder-action/32/folder_add-512.png' - - raise ValueError('Not working version of action') - @async - def createFoldersFromEntity(self, entity): - '''Generate folder structure from *entity*. - - Entity is assumed to be either a project, episode, sequence or shot. - - ''' - - root = entity.getProject().getRoot() - - self.logger.info(root) - - if entity.getObjectType() in ( - 'Episode', 'Sequence', 'Folder', 'Shot'): - objects = entity.getChildren(objectType='Shot', depth=None) - objects.append(entity) - else: - objects = entity.getChildren(depth=None) - - for obj in objects: - - tasks = obj.getTasks() - paths_collected = set([]) - if obj.getObjectType() in ( - 'Episode', 'Sequence', 'Shot', 'Folder'): - task_mask = 'shot.task' - else: - task_mask = 'asset.task' - - self.logger.info(task_mask) - - for task in tasks: - self.logger.info(task) - paths = ft_utils.getAllPathsYaml(task) - self.logger.info(paths) - for path in paths: - if task_mask in path[1].name: - temppath = os.path.join( - root, path[0].lower().replace(" ", '_').replace('\'', '')) - paths_collected.add(temppath) - - for path in paths_collected: - self.logger.info(path) - try: - os.makedirs(path) - except OSError as error: - if error.errno != errno.EEXIST: - raise - - - def discover(self, session, entities, event): - - if len(entities) == 0 or entities[0].entity_type not in [ - 'Episode', 'Sequence', 'Shot', 'Folder', 'Asset Build']: - return False - - return True - - - def launch(self, session, entities, event): - - 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': 'Creating Folders' - }) - }) - - '''Callback method for custom action.''' - - try: - session.event_hub.publishReply( - event, - data={ - 'success': True, - 'message': 'Folder Creation Job Started!' - } - ) - - for entity in entities: - self.createFoldersFromEntity(entity) - - job.setStatus('done') - except: - job.setStatus('failed') - raise - - - return { - 'success': True, - 'message': 'Created Folders Successfully!' - } - - -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 = CreateFolders(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:])) diff --git a/pype/ftrack/actions/action_open_folder.py b/pype/ftrack/actions/action_open_folder.py deleted file mode 100644 index 6c582adf2a..0000000000 --- a/pype/ftrack/actions/action_open_folder.py +++ /dev/null @@ -1,163 +0,0 @@ -import sys -import argparse -import logging -import getpass -import subprocess -import os - -import ftrack_api -from ftrack_action_handler import BaseAction -import ft_utils - -class openFolder(BaseAction): - '''Open folders action''' - - #: Action identifier. - identifier = 'open.folders' - #: Action label. - label = 'Open Folders' - #: Action Icon. - icon = "https://cdn3.iconfinder.com/data/icons/stroke/53/Open-Folder-256.png" - raise ValueError('Not working version of action') - - def discover(self, session, entities, event): - ''' Validation ''' - - if len(entities) == 0 or entities[0].entity_type in ['assetversion', 'Component']: - return False - - return True - - - def get_paths(self, entity): - '''Prepare all the paths for the entity. - - This function uses custom module to deal with paths. - You will need to replace it with your logic. - ''' - - root = entity['project']['root'] - entity_type = entity.entity_type.lower() - - if entity_type == 'task': - if entity['parent'].entity_type == 'Asset Build': - templates = ['asset.task'] - else: - templates = ['shot.task'] - - elif entity_type in ['shot', 'folder', 'sequence', 'episode']: - templates = ['shot'] - - elif entity_type in ['asset build', 'library']: - templates = ['asset'] - - paths = ft_utils.getPathsYaml(entity, - templateList=templates, - root=root) - return paths - - def launch(self, session, entities, event): - '''Callback method for action.''' - selection = event['data'].get('selection', []) - self.logger.info(u'Launching action with selection \ - {0}'.format(selection)) - - # Prepare lists to keep track of failures and successes - fails = [] - hits = set([]) - - for entity in entities: - - # Get paths base on the entity. - # This function needs to be chagned to fit your path logic - paths = self.get_paths(entity) - - # For each path, check if it exists on the disk and try opening it - for path in paths: - if os.path.isdir(path): - self.logger.info('Opening: ' + path) - - # open the folder - if sys.platform == 'darwin': - subprocess.Popen(['open', '--', path]) - elif sys.platform == 'linux2': - subprocess.Popen(['gnome-open', '--', path]) - elif sys.platform == 'win32': - subprocess.Popen(['explorer', path]) - - # add path to list of hits - hits.add(entity['name']) - - # Add entity to fails list if no folder could be openned for it - if entity['name'] not in hits: - fails.append(entity['name']) - - # Inform user of the result - if len(hits) == 0: - return { - 'success': False, - 'message': 'No folders found for: {}'.format(', '.join(fails)) - } - - if len(fails) > 0: - return { - 'success': True, - 'message': 'No folders found for: {}'.format(', '.join(fails)) - } - - return { - 'success': True, - 'message': 'Opening folders' - } - - -def register(session, **kw): - '''Register action. Called when used as an event 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 = openFolder(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:])) diff --git a/pype/ftrack/actions/batch_create.py b/pype/ftrack/actions/batch_create.py deleted file mode 100644 index c17596367c..0000000000 --- a/pype/ftrack/actions/batch_create.py +++ /dev/null @@ -1,349 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2015 ftrack -import sys -import argparse -import logging -import collections -import threading -import getpass -import ftrack_api -from ftrack_action_handler import BaseAction - -STRUCTURE_NAMES = ['episode', 'sequence', 'shot'] - - -TASK_TYPE_ENUMERATOR_OPTIONS = [ - {'label': task_type.getName(), 'value': task_type.getId()} - for task_type in ftrack.getTaskTypes() -] - -TASK_TYPE_LOOKUP = dict( - (task_type.getId(), task_type.getName()) - for task_type in ftrack.getTaskTypes() -) - - -def async(fn): - '''Run *fn* asynchronously.''' - def wrapper(*args, **kwargs): - thread = threading.Thread(target=fn, args=args, kwargs=kwargs) - thread.start() - return wrapper - - -def get_names(base_name, padding, start, end, incremental): - '''Return names from expression.''' - names = [] - for part in range(start, end + incremental, incremental): - names.append( - base_name + str(part).zfill(padding) - ) - return names - - -def generate_structure(values): - '''Return structure from *values*.''' - structure = [] - - for structure_name in STRUCTURE_NAMES: - if (structure_name + '_expression') not in values: - continue - - object_expression = values[structure_name + '_expression'] - object_incremental = values[structure_name + '_incremental'] - - padding = object_expression.count('#') - _range, incremental = object_incremental.split(':') - start, end = _range.split('-') - - start = int(start) - end = int(end) - incremental = int(incremental) - - base_name = object_expression.replace('#', '') - - logging.info( - ( - 'Create from expression {expression} with {base_name}, ' - '{padding} and {start}-{end}:{incremental}' - ).format( - expression=object_expression, - base_name=base_name, - padding=padding, - start=start, - end=end, - incremental=incremental - ) - ) - - names = get_names( - base_name=base_name, - padding=padding, - start=start, - end=end, - incremental=incremental - ) - - structure.append({ - 'object_type': structure_name, - 'data': names - }) - - tasks = collections.defaultdict(dict) - for name, value in values.iteritems(): - if name.startswith('task_'): - _, index, key = name.split('_') - if key == 'bid' and value: - value = float(value) * 3600 - tasks[index][key] = value - - task_data = [] - structure.append({ - 'object_type': 'task', - 'data': task_data - }) - for task in tasks.values(): - task_data.append(task) - - return structure - - -@async -def create(parent, structure): - '''Create *structure* under *parent*.''' - return create_from_structure(parent, structure) - - -def create_from_structure(parent, structure): - '''Create *structure* under *parent*.''' - level = structure[0] - children = structure[1:] - object_type = level['object_type'] - - for data in level['data']: - - if object_type == 'episode': - new_object = parent.createEpisode(data) - - if object_type == 'sequence': - new_object = parent.createSequence(data) - - if object_type == 'shot': - new_object = parent.createShot(data) - - if object_type == 'task': - new_object = parent.createTask( - TASK_TYPE_LOOKUP[data['typeid']] - ) - new_object.set(data) - - logging.info( - 'Created {new_object} on parent {parent}'.format( - parent=parent, new_object=new_object - ) - ) - if children: - create_from_structure(new_object, children) - - -def get_form(number_of_tasks, structure_type, prefix, padding_count): - '''Return form from *number_of_tasks* and *structure_type*.''' - mappings = { - 'episode': ['episode', 'sequence', 'shot'], - 'sequence': ['sequence', 'shot'], - 'shot': ['shot'] - } - - items = [] - - for structure_name in mappings[structure_type]: - items.extend( - [ - { - 'value': '##{0}##'.format(structure_name.capitalize()), - 'type': 'label' - }, { - 'label': 'Expression', - 'type': 'text', - 'value': prefix + '#' * padding_count, - 'name': '{0}_expression'.format(structure_name) - }, { - 'label': 'Incremental', - 'type': 'text', - 'value': '10-20:10', - 'name': '{0}_incremental'.format(structure_name) - } - ] - ) - - for index in range(0, number_of_tasks): - items.extend( - [ - { - 'value': '##Template for Task{0}##'.format(index), - 'type': 'label' - }, - { - 'label': 'Type', - 'type': 'enumerator', - 'name': 'task_{0}_typeid'.format(index), - 'data': TASK_TYPE_ENUMERATOR_OPTIONS - }, - { - 'label': 'Bid', - 'type': 'number', - 'name': 'task_{0}_bid'.format(index) - } - ] - ) - - return {'items': items} - - -class BatchCreate(BaseAction): - '''Batch create objects in ftrack.''' - - #: Action identifier. - identifier = 'batch_create' - #: Action label. - label = 'Batch create' - - def discover(self, session, entities, event): - - if (len(entities) != 1 or entities[0].entity_type.lower() - not in ['project', 'episode', 'sequence']): - return False - - return True - - def interface(self, session, entities, event): - if 'values' not in event['data']: - data = [ - { - 'label': 'Episode, Sequence, Shot', - 'value': 'episode' - }, { - 'label': 'Sequence, Shot', - 'value': 'sequence' - }, { - 'label': 'Shot', - 'value': 'shot' - } - ] - entity = None - data_value = 'episode' - entity_name = '' - try: - entity = ftrack.Project(selection[0]['entityId']) - entity_name = entity.getFullName() - except: - pass - try: - entity = ftrack.Task(selection[0]['entityId']) - object_type = entity.getObjectType() - entity_name = entity.getName() - - if object_type == 'Episode': - del data[0] - data_value = 'sequence' - - if object_type == 'Sequence': - del data[0] - del data[0] - data_value = 'shot' - except: - pass - return [ - { - 'label': 'Select structure', - 'type': 'enumerator', - 'value': data_value, - 'name': 'structure_type', - 'data': data - }, { - 'label': 'Padding count', - 'type': 'number', - 'name': 'padding_count', - 'value': 4 - }, { - 'label': 'Number of tasks', - 'type': 'number', - 'name': 'number_of_tasks', - 'value': 2 - } - ] - - def launch(self, session, entities, event): - '''Callback method for action.''' - selection = event['data'].get('selection', []) - values = event['data'].get('values', {}) - if values: - if 'number_of_tasks' in values: - form = get_form( - int(values['number_of_tasks']), - values['structure_type'], - entity_name + '_', - int(values['padding_count']) - ) - return form - - else: - structure = generate_structure(values) - logging.info('Creating structure "{0}"'.format(str(structure))) - create(entity, structure) - return { - 'success': True, - 'message': 'Action completed successfully' - } - - -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 = BatchCreate(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:])) diff --git a/pype/ftrack/actions/djvview.py b/pype/ftrack/actions/djvview.py index 87213c1308..31c1812662 100644 --- a/pype/ftrack/actions/djvview.py +++ b/pype/ftrack/actions/djvview.py @@ -1,9 +1,7 @@ import logging import subprocess import sys -import pprint import os -import getpass import re from operator import itemgetter import ftrack_api @@ -72,7 +70,7 @@ class DJVViewAction(object): } def register(self): - '''Registers the action, subscribing the the discover and launch topics.''' + '''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 @@ -91,19 +89,19 @@ class DJVViewAction(object): def get_applications(self): applications = [] - label="DJVView {version}" - versionExpression=re.compile(r"(?P\d+.\d+.\d+)") - applicationIdentifier="djvview" - description="DJV View Launcher" - icon="http://a.fsdn.com/allura/p/djv/icon" + label = "DJVView {version}" + versionExpression = re.compile(r"(?P\d+.\d+.\d+)") + applicationIdentifier = "djvview" + description = "DJV View Launcher" + icon = "http://a.fsdn.com/allura/p/djv/icon" expression = [] if sys.platform == "win32": expression = ["C:\\", "Program Files", "djv-\d.+", - "bin", "djv_view.exe"] + "bin", "djv_view.exe"] elif sys.platform == "darwin": expression = ["Application", "DJV.app", "Contents", "MacOS", "DJV"] - ## Linuxs + # Linuxs else: expression = ["usr", "local", "djv", "djv_view"] @@ -218,38 +216,37 @@ class DJVViewAction(object): end = 379 fps = 24 # TODO issequence is probably already built-in validation in ftrack - isseq = re.findall( '%[0-9]*d', filename ) + isseq = re.findall('%[0-9]*d', filename) if len(isseq) > 0: - padding = re.findall( '%[0-9]*d', filename ).pop() - range = ( padding % start ) + '-' + ( padding % end ) - filename = re.sub( '%[0-9]*d', range, filename ) - + padding = re.findall('%[0-9]*d', filename).pop() + range = (padding % start) + '-' + (padding % end) + filename = re.sub('%[0-9]*d', range, filename) cmd = [] # DJV path - cmd.append( os.path.normpath( self.djv_path ) ) - ### DJV Options Start ################################################ - # cmd.append( '-file_layer (value)' ) #layer name - cmd.append( '-file_proxy 1/2' ) #Proxy scale: 1/2, 1/4, 1/8 - cmd.append( '-file_cache True' ) # Cache: True, False. - # cmd.append( '-window_fullscreen' ) #Start in full screen + cmd.append(os.path.normpath(self.djv_path)) + # DJV Options Start ############################################## + # cmd.append('-file_layer (value)') #layer name + cmd.append('-file_proxy 1/2') # Proxy scale: 1/2, 1/4, 1/8 + cmd.append('-file_cache True') # Cache: True, False. + # cmd.append('-window_fullscreen') #Start in full screen # cmd.append("-window_toolbar False") # Toolbar controls: False, True. # cmd.append("-window_playbar False") # Window controls: False, True. # cmd.append("-view_grid None") # Grid overlay: None, 1x1, 10x10, 100x100. # cmd.append("-view_hud True") # Heads up display: True, False. - cmd.append("-playback Forward") # Playback: Stop, Forward, Reverse. + cmd.append("-playback Forward") # Playback: Stop, Forward, Reverse. # cmd.append("-playback_frame (value)") # Frame. cmd.append("-playback_speed " + str(fps)) # cmd.append("-playback_timer (value)") # Timer: Sleep, Timeout. Value: Sleep. # cmd.append("-playback_timer_resolution (value)") # Timer resolution (seconds): 0.001. - cmd.append("-time_units Frames") # Time units: Timecode, Frames. - ### DJV Options End ################################################## + cmd.append("-time_units Frames") # Time units: Timecode, Frames. + # DJV Options End ################################################ # PATH TO COMPONENT - cmd.append( os.path.normpath( filename ) ) + cmd.append(os.path.normpath(filename)) # Run DJV with these commands - subprocess.Popen( ' '.join( cmd ) ) + subprocess.Popen(' '.join(cmd)) return { 'success': True, @@ -297,7 +294,7 @@ class DJVViewAction(object): try: # TODO This is proper way to get filepath!!! - # NOT WORKING RIGHT NOW + # THIS WON'T WORK RIGHT NOW location = component['component_locations'][0]['location'] file_path = location.get_filesystem_path(component) # if component.isSequence(): @@ -305,7 +302,7 @@ class DJVViewAction(object): # frame = int(component.getMembers()[0].getName()) # file_path = file_path % frame except: - # This is NOT proper way + # This works but is NOT proper way file_path = component['component_locations'][0]['resource_identifier'] event["data"]["items"].append( @@ -334,8 +331,6 @@ class DJVViewAction(object): } - - def register(session, **kw): """Register hooks.""" if not isinstance(session, ftrack_api.session.Session): @@ -344,6 +339,7 @@ def register(session, **kw): action = DJVViewAction(session) action.register() + def main(arguments=None): '''Set up logging and register action.''' if arguments is None: diff --git a/pype/ftrack/actions/ftrack_action_handler.py b/pype/ftrack/actions/ftrack_action_handler.py index d1e2ea58df..18b4d17196 100644 --- a/pype/ftrack/actions/ftrack_action_handler.py +++ b/pype/ftrack/actions/ftrack_action_handler.py @@ -16,7 +16,6 @@ from app.api import ( t = Templates() - class AppAction(object): '''Custom Action base class @@ -50,14 +49,13 @@ class AppAction(object): self.icon = icon self.description = description - @property def session(self): '''Return current session.''' return self._session def register(self): - '''Registers the action, subscribing the the discover and launch topics.''' + '''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 @@ -72,7 +70,6 @@ class AppAction(object): self._launch ) - def _discover(self, event): args = self._translate_event( self.session, event @@ -101,11 +98,10 @@ class AppAction(object): *session* is a `ftrack_api.Session` instance - - *entities* is a list of tuples each containing the entity type and the entity id. - If the entity is a hierarchical you will always get the entity - type TypedContext, once retrieved through a get operation you - will have the "real" entity type ie. example Shot, Sequence + *entities* is a list of tuples each containing the entity type and + the entity id. If the entity is a hierarchical you will always get + the entity type TypedContext, once retrieved through a get operation + you will have the "real" entity type ie. example Shot, Sequence or Asset Build. *event* the unmodified original event @@ -230,8 +226,8 @@ class AppAction(object): entity = session.get(entity, id) silo = "Film" - if entity.entity_type=="AssetBuild": - silo= "Asset" + if entity.entity_type == "AssetBuild": + silo = "Asset" # set environments for Avalon os.environ["AVALON_PROJECT"] = entity['project']['full_name'] @@ -241,24 +237,22 @@ class AppAction(object): os.environ["AVALON_APP"] = self.identifier os.environ["AVALON_APP_NAME"] = self.identifier + "_" + self.variant - anatomy = t.anatomy io.install() - hierarchy = io.find_one({"type":'asset', "name":entity['parent']['name']})['data']['parents'] + hierarchy = io.find_one({"type": 'asset', "name": entity['parent']['name']})['data']['parents'] io.uninstall() if hierarchy: # hierarchy = os.path.sep.join(hierarchy) hierarchy = os.path.join(*hierarchy) - data = { "project": {"name": entity['project']['full_name'], + data = {"project": {"name": entity['project']['full_name'], "code": entity['project']['name']}, - "task": entity['name'], - "asset": entity['parent']['name'], - "hierarchy": hierarchy} + "task": entity['name'], + "asset": entity['parent']['name'], + "hierarchy": hierarchy} anatomy = anatomy.format(data) - os.environ["AVALON_WORKDIR"] = os.path.join(anatomy.work.root, anatomy.work.folder) # TODO Add paths to avalon setup from tomls diff --git a/pype/ftrack/create_custAttributes_AvalonId.py b/pype/ftrack/create_custAttributes_AvalonId.py index 01f6d7b4d6..a36f56940c 100644 --- a/pype/ftrack/create_custAttributes_AvalonId.py +++ b/pype/ftrack/create_custAttributes_AvalonId.py @@ -3,11 +3,7 @@ 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", -) +session = ftrack_api.Session() objTypes = set() diff --git a/pype/ftrack/ftrack_utils.py b/pype/ftrack/ftrack_utils.py index a7bf8efc17..23531a9fdd 100644 --- a/pype/ftrack/ftrack_utils.py +++ b/pype/ftrack/ftrack_utils.py @@ -1,11 +1,15 @@ # fttrack help functions - -# import ftrack +import ftrack_api import os from pprint import * +def checkLogin(): + # check Environments FTRACK_API_USER, FTRACK_API_KEY + pass + + def checkRegex(): # _handle_result -> would be solution? # """ TODO Check if name of entities match REGEX""" From 75a67bb092ff6a3bd1b21dc3535b32f611838e6b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 7 Nov 2018 15:30:13 +0100 Subject: [PATCH 06/16] action "Create Custom attributes" wasn't probably pushed (done again) --- .../actions/action_createCustomAttributes.py | 205 ++++++------------ pype/ftrack/create_custAttributes_AvalonId.py | 47 ---- 2 files changed, 72 insertions(+), 180 deletions(-) delete mode 100644 pype/ftrack/create_custAttributes_AvalonId.py diff --git a/pype/ftrack/actions/action_createCustomAttributes.py b/pype/ftrack/actions/action_createCustomAttributes.py index afcb0a812d..214112d15f 100644 --- a/pype/ftrack/actions/action_createCustomAttributes.py +++ b/pype/ftrack/actions/action_createCustomAttributes.py @@ -34,116 +34,9 @@ class AvalonIdAttribute(BaseAction): return True - 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"] - os.environ["AVALON_ASSET"] = entityProj['full_name'] - - # Get apps from Ftrack / TODO Exceptions?!!! - apps = [] - for app in entityProj['custom_attributes']['applications']: - try: - label = toml.load(lib.which_app(app))['label'] - apps.append({'name':app, 'label':label}) - except Exception as e: - print('Error with application {0} - {1}'.format(app, e)) - - # Set project Config - config = { - 'schema': 'avalon-core:config-1.0', - 'tasks': [{'name': ''}], - 'apps': apps, - 'template': {'work': '','publish':''} - } - - # Set project template - template = {"schema": "avalon-core:inventory-1.0"} - - # --- Create project and assets in Avalon --- - io.install() - # If project don't exists -> ELSE - if (io.find_one( - {'type': 'project', 'name': entityProj['full_name']}) is None): - inventory.save(entityProj['full_name'], config, template) - else: - io.update_many({'type': 'project','name': entityProj['full_name']}, - {'$set':{'config':config}}) - - # Store info about project (FtrackId) - io.update_many({'type': 'project','name': entityProj['full_name']}, - {'$set':{'data':{'ftrackId':entityProj['id'],'entityType':entityProj.entity_type}}}) - - # 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() @@ -151,44 +44,90 @@ class AvalonIdAttribute(BaseAction): 'user': user, 'status': 'running', 'data': json.dumps({ - 'description': 'Synch Ftrack to Avalon.' + 'description': 'Custom Attribute creation.' }) }) - + session.commit() 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 = [] + # Attribute Name and Label + custAttrName = 'avalon_mongo_id' + custAttrLabel = 'Avalon/Mongo Id' + # Types that don't need object_type_id + base = {'show','asset','assetversion'} + # Don't create custom attribute on these entity types: + exceptions = ['task','milestone','library'] + exceptions.extend(base) + # Get all possible object types + all_obj_types = session.query('ObjectType').all() + count_types = len(all_obj_types) + # Filter object types by exceptions + for index in range(count_types): + i = count_types - 1 - index + name = all_obj_types[i]['name'].lower() - def getShotAsset(entity): - if not (entity.entity_type in ['Task']): - if entity not in importable: - importable.append(entity) + if " " in name: + name = name.replace(" ","") - if entity['children']: - childrens = entity['children'] - for child in childrens: - getShotAsset(child) + if name in exceptions: + all_obj_types.pop(i) - # get all entities separately - for entity in entities: - entity_type, entity_id = entity - act_ent = session.get(entity_type, entity_id) - getShotAsset(act_ent) + # Get IDs of filtered object types + all_obj_types_id = set() + for obj in all_obj_types: + all_obj_types_id.add(obj['id']) - for e in importable: - self.importToAvalon(session, e) + # Get all custom attributes + current_cust_attr = session.query('CustomAttributeConfiguration').all() + # Filter already existing AvalonMongoID attr. + for attr in current_cust_attr: + if attr['key'] == custAttrName: + if attr['entity_type'] in base: + base.remove(attr['entity_type']) + if attr['object_type_id'] in all_obj_types_id: + all_obj_types_id.remove(attr['object_type_id']) + + # Set session back to begin("session.query" raises error on commit) + session.rollback() + # Set security roles for attribute + custAttrSecuRole = session.query('SecurityRole').all() + # Set Text type of Attribute + custom_attribute_type = session.query( + 'CustomAttributeType where name is "text"' + ).one() + + for entity_type in base: + # Create a custom attribute configuration. + session.create('CustomAttributeConfiguration', { + 'entity_type': entity_type, + 'type': custom_attribute_type, + 'label': custAttrLabel, + 'key': custAttrName, + 'default': '', + 'write_security_roles': custAttrSecuRole, + 'read_security_roles': custAttrSecuRole, + 'config': json.dumps({'markdown': False}) + }) + + for type in all_obj_types_id: + # Create a custom attribute configuration. + session.create('CustomAttributeConfiguration', { + 'entity_type': 'task', + 'object_type_id': type, + 'type': custom_attribute_type, + 'label': custAttrLabel, + 'key': custAttrName, + 'default': '', + 'write_security_roles': custAttrSecuRole, + 'read_security_roles': custAttrSecuRole, + 'config': json.dumps({'markdown': False}) + }) 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("Creating custom attributes failed") print(e) return True diff --git a/pype/ftrack/create_custAttributes_AvalonId.py b/pype/ftrack/create_custAttributes_AvalonId.py deleted file mode 100644 index a36f56940c..0000000000 --- a/pype/ftrack/create_custAttributes_AvalonId.py +++ /dev/null @@ -1,47 +0,0 @@ -import ftrack_utils - -import ftrack_api - - -session = ftrack_api.Session() - -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() From 3b8b9a23bcbf2f8392e836bf173dbbfce1e5ce89 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 8 Nov 2018 19:07:47 +0100 Subject: [PATCH 07/16] Basic template for Login to Ftrack --- pype/ftrack/Login/__init__.py | 6 + pype/ftrack/Login/credentials.py | 49 ++ pype/ftrack/Login/login_dialog.py | 168 +++++++ pype/ftrack/Login/login_setup.py | 442 ++++++++++++++++++ pype/ftrack/Login/login_tools.py | 107 +++++ .../Login/templatesforcode/login_dialogue.py | 198 ++++++++ .../templatesforcode/login_dialogue_usage.py | 196 ++++++++ pype/ftrack/__init__.py | 1 + 8 files changed, 1167 insertions(+) create mode 100644 pype/ftrack/Login/__init__.py create mode 100644 pype/ftrack/Login/credentials.py create mode 100644 pype/ftrack/Login/login_dialog.py create mode 100644 pype/ftrack/Login/login_setup.py create mode 100644 pype/ftrack/Login/login_tools.py create mode 100644 pype/ftrack/Login/templatesforcode/login_dialogue.py create mode 100644 pype/ftrack/Login/templatesforcode/login_dialogue_usage.py diff --git a/pype/ftrack/Login/__init__.py b/pype/ftrack/Login/__init__.py new file mode 100644 index 0000000000..80bdb7f70f --- /dev/null +++ b/pype/ftrack/Login/__init__.py @@ -0,0 +1,6 @@ +import os +import sys +import credentials +import login_dialog + +# login_dialogue_usage.main() diff --git a/pype/ftrack/Login/credentials.py b/pype/ftrack/Login/credentials.py new file mode 100644 index 0000000000..9f409c5349 --- /dev/null +++ b/pype/ftrack/Login/credentials.py @@ -0,0 +1,49 @@ +import os +import toml +# import ftrack_api + +# TODO JUST TEST PATH - path should be in Environment Variables... +config_path = r"C:\test" +config_name = 'credentials.toml' +fpath = os.path.join(config_path, config_name) + +def _get_credentials(): + try: + file = open(fpath, 'r') + except: + file = open(fpath, 'w') + + credentials = toml.load(file) + file.close() + + return credentials + +def _save_credentials(username, apiKey): + file = open(fpath, 'w') + + data = { + 'username':username, + 'apiKey':apiKey + } + + credentials = toml.dumps(data) + file.write(credentials) + file.close() + +def _clear_credentials(): + file = open(fpath, 'w').close() + +def _set_env(username, apiKey): + os.environ['FTRACK_API_USER'] = username + os.environ['FTRACK_API_KEY'] = apiKey + +def _check_credentials(username, apiKey): + + _set_env(username, apiKey) + + try: + session = ftrack_api.Session() + return True + except Exception as e: + print(e) + return False diff --git a/pype/ftrack/Login/login_dialog.py b/pype/ftrack/Login/login_dialog.py new file mode 100644 index 0000000000..a7186ab415 --- /dev/null +++ b/pype/ftrack/Login/login_dialog.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file '..\CODE\github\pypeclub\pype-setup\temp\pype_project_settins_ui\login_dialogue.ui' +# +# Created by: PyQt5 UI code generator 5.7.1 +# +# WARNING! All changes made in this file will be lost! + +import sys +from PyQt5 import QtCore, QtGui, QtWidgets +from app import style +import credentials +import login_tools + +class Login_Dialog_ui(QtWidgets.QWidget): + + SIZE_W = 300 + SIZE_H = 160 + + def __init__(self): + super().__init__() + + _translate = QtCore.QCoreApplication.translate + + self.resize(self.SIZE_W, self.SIZE_H) + self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) + self.setStyleSheet(style.load_stylesheet()) + + self.main = QtWidgets.QVBoxLayout() + self.main.setObjectName("main") + + self.form = QtWidgets.QFormLayout() + self.form.setContentsMargins(10, 15, 10, 5) + self.form.setObjectName("form") + + font = QtGui.QFont() + font.setFamily("DejaVu Sans Condensed") + font.setPointSize(9) + font.setBold(True) + font.setWeight(50) + font.setKerning(True) + + self.ftsite_label = QtWidgets.QLabel("FTrack URL:") + self.ftsite_label.setFont(font) + self.ftsite_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.ftsite_label.setTextFormat(QtCore.Qt.RichText) + self.ftsite_label.setObjectName("user_label") + + self.ftsite_input = QtWidgets.QLineEdit() + self.ftsite_input.setEnabled(True) + self.ftsite_input.setFrame(True) + self.ftsite_input.setEnabled(False) + self.ftsite_input.setReadOnly(True) + self.ftsite_input.setObjectName("ftsite_input") + + self.user_label = QtWidgets.QLabel("Username:") + self.user_label.setFont(font) + self.user_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.user_label.setTextFormat(QtCore.Qt.RichText) + self.user_label.setObjectName("user_label") + + self.user_input = QtWidgets.QLineEdit() + self.user_input.setEnabled(True) + self.user_input.setFrame(True) + self.user_input.setObjectName("user_input") + self.user_input.setPlaceholderText(_translate("main","user.name")) + + self.api_label = QtWidgets.QLabel("API Key:") + self.api_label.setFont(font) + self.api_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.api_label.setTextFormat(QtCore.Qt.RichText) + self.api_label.setObjectName("api_label") + + self.api_input = QtWidgets.QLineEdit() + self.api_input.setEnabled(True) + self.api_input.setFrame(True) + self.api_input.setObjectName("api_input") + self.api_input.setPlaceholderText(_translate("main","e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")) + + self.form.addRow(self.ftsite_label, self.ftsite_input) + self.form.addRow(self.user_label, self.user_input) + self.form.addRow(self.api_label,self.api_input) + + self.btnGroup = QtWidgets.QHBoxLayout() + self.btnGroup.addStretch(1) + self.btnGroup.setObjectName("btnGroup") + + self.btnEnter = QtWidgets.QPushButton("Login") + self.btnEnter.setToolTip('Set Username and API Key with entered values') + self.btnEnter.clicked.connect(self._enter_credentials) + + self.btnClose = QtWidgets.QPushButton("Close") + self.btnClose.setToolTip('Close this window') + self.btnClose.clicked.connect(self._close_widget) + + self.btnFtrack = QtWidgets.QPushButton("Ftrack") + self.btnFtrack.setToolTip('Open browser for Login to Ftrack') + self.btnFtrack.clicked.connect(self._open_ftrack) + + self.btnGroup.addWidget(self.btnFtrack) + self.btnGroup.addWidget(self.btnEnter) + self.btnGroup.addWidget(self.btnClose) + + self.main.addLayout(self.form) + self.main.addLayout(self.btnGroup) + + self.setLayout(self.main) + self.setWindowTitle('FTrack Login') + self._set_site() + self.show() + + def _set_site(self): + try: + txt = os.getenv('FTRACK_SERVER') + except: + txt = "FTrack site si is not set!" + + self.ftsite_input.setText(txt) + + def _enter_credentials(self): + print("EnteredCredentials!") + user = self.user_input.text() + api = self.api_input.text() + verification = credentials._check_credentials(user, api) + + if verification: + print("SUCCESS") + credentials._save_credentials(user, api) + credentials._set_env(user, api) + self._close_widget() + + def _open_ftrack(self): + print("OpenWindow!") + try: + url = "pype.ftrackapp.com" + self.loginSignal = QtCore.pyqtSignal(object, object, object) + self._login_server_thread = login_tools.LoginServerThread() + self._login_server_thread.loginSignal.connect(self.loginSignal) + self._login_server_thread.start(url) + except Exception as e: + print(e) + + def _close_widget(self): + sys.exit(app.exec_()) + + +class Login_Dialog(Login_Dialog_ui): + def __init__(self): + super(Login_Dialog, self).__init__() + + def execute(self): + self._check_credentials() + + +def getApp(): + return QtWidgets.QApplication(sys.argv) + +def main(): + app = getApp() + ui = Login_Dialog() + ui.show() + sys.exit(app.exec_()) + + +if __name__ == "__main__": + main() + +main() diff --git a/pype/ftrack/Login/login_setup.py b/pype/ftrack/Login/login_setup.py new file mode 100644 index 0000000000..6ebae672fa --- /dev/null +++ b/pype/ftrack/Login/login_setup.py @@ -0,0 +1,442 @@ +import os +import sys +import login_tools +from PyQt5 import QtCore +import requests + + +class FtrackLogin(object): + + # loginSignal = QtCore.pyqtSignal(object, object, object) + # + # def __init__(self): + # self.username = None + # self.apiKey = None + # self.url = "https://pype.ftrackapp.com" + # + # self._login_server_thread = None + # self.loginSignal.connect(self.loginWithCredentials) + # self.login() + # + # def login(self): + # '''Login using stored credentials or ask user for them.''' + # + # credentials = self._get_credentials() + # if credentials: + # # Try to login. + # self.loginWithCredentials( + # credentials['server_url'], + # credentials['api_user'], + # credentials['api_key'] + # ) + # + # def setup_session(self): + # try: + # session = ftrack_api.session() + # except Exception as e: + # return False + # return session + # + # def report_session_setup_error(self, error): + # msg = ( + # u'\nAn error occured while starting ftrack: {0}.'.format(error) + # ) + # print(msg) + # # self.loginError.emit(msg) + # + # def _get_credentials(self): + # data = {'server_url':self.url, + # 'api_user':self.username, + # 'api_key':self.apiKey + # } + # return data + # + # def _save_credentials(self, url, username, apiKey): + # self.url = url + # self.username = username + # self.apiKey = apiKey + # + # def loginWithCredentials(self, url, username, apiKey): + # url = url.strip('/ ') + # + # if not url: + # self.loginError.emit( + # '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: + # self.logger.exception('Error reaching server url.') + # self.loginError.emit( + # 'The server URL you provided could not be reached.' + # ) + # return + # + # if ( + # result.status_code != 200 or 'FTRACK_VERSION' not in result.headers + # ): + # self.loginError.emit( + # '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) + # return + # + # # Set environment variables supported by the old API. + # os.environ['FTRACK_SERVER'] = url + # os.environ['LOGNAME'] = username + # os.environ['FTRACK_APIKEY'] = apiKey + # + # # Set environment variables supported by the new API. + # os.environ['FTRACK_API_USER'] = username + # os.environ['FTRACK_API_KEY'] = apiKey + # + # # Login using the new ftrack API. + # try: + # self._session = self._setup_session() + # except Exception as error: + # self.logger.exception(u'Error during login.:') + # self._report_session_setup_error(error) + # return + # + # # Store credentials since login was successful. + # self._save_credentials(url, username, apiKey) + # + # # Verify storage scenario before starting. + # if 'storage_scenario' in self._session.server_information: + # storage_scenario = self._session.server_information.get( + # 'storage_scenario' + # ) + # if storage_scenario is None: + # # Hide login overlay at this time since it will be deleted + # self.logger.debug('Storage scenario is not configured.') + # scenario_widget = _scenario_widget.ConfigureScenario( + # self._session + # ) + # scenario_widget.configuration_completed.connect( + # self.location_configuration_finished + # ) + # self.setCentralWidget(scenario_widget) + # self.focus() + # return + + + + + + + + + + + + + + + + + + + + + + + + + + + loginError = QtCore.pyqtSignal(object) + + #: Signal when event received via ftrack's event hub. + eventHubSignal = QtCore.pyqtSignal(object) + + # Login signal. + loginSignal = QtCore.pyqtSignal(object, object, object) + + def __init__(self, *args, **kwargs): + + # self.logger = logging.getLogger( + # __name__ + '.' + self.__class__.__name__ + # ) + + self._login_server_thread = None + + self._login_overlay = None + self.loginSignal.connect(self.loginWithCredentials) + self.login() + + + def _onConnectTopicEvent(self, event): + '''Generic callback for all ftrack.connect events. + + .. note:: + Events not triggered by the current logged in user will be dropped. + + ''' + if event['topic'] != 'ftrack.connect': + return + + self._routeEvent(event) + + def logout(self): + '''Clear stored credentials and quit Connect.''' + self._clear_qsettings() + config = ftrack_connect.ui.config.read_json_config() + + config['accounts'] = [] + ftrack_connect.ui.config.write_json_config(config) + + QtWidgets.qApp.quit() + + def _clear_qsettings(self): + '''Remove credentials from QSettings.''' + settings = QtCore.QSettings() + settings.remove('login') + + def _get_credentials(self): + '''Return a dict with API credentials from storage.''' + credentials = None + + # Read from json config file. + json_config = ftrack_connect.ui.config.read_json_config() + if json_config: + try: + data = json_config['accounts'][0] + credentials = { + 'server_url': data['server_url'], + 'api_user': data['api_user'], + 'api_key': data['api_key'] + } + except Exception: + self.logger.debug( + u'No credentials were found in config: {0}.'.format( + json_config + ) + ) + + # Fallback on old QSettings. + if not json_config and not credentials: + settings = QtCore.QSettings() + server_url = settings.value('login/server', None) + api_user = settings.value('login/username', None) + api_key = settings.value('login/apikey', None) + + if not None in (server_url, api_user, api_key): + credentials = { + 'server_url': server_url, + 'api_user': api_user, + 'api_key': api_key + } + + return credentials + + def _save_credentials(self, server_url, api_user, api_key): + '''Save API credentials to storage.''' + # Clear QSettings since they should not be used any more. + self._clear_qsettings() + + # Save the credentials. + json_config = ftrack_connect.ui.config.read_json_config() + + if not json_config: + json_config = {} + + # Add a unique id to the config that can be used to identify this + # machine. + if not 'id' in json_config: + json_config['id'] = str(uuid.uuid4()) + + json_config['accounts'] = [{ + 'server_url': server_url, + 'api_user': api_user, + 'api_key': api_key + }] + + ftrack_connect.ui.config.write_json_config(json_config) + + def login(self): + '''Login using stored credentials or ask user for them.''' + credentials = self._get_credentials() + self.showLoginWidget() + + if credentials: + # Try to login. + self.loginWithCredentials( + credentials['server_url'], + credentials['api_user'], + credentials['api_key'] + ) + + def showLoginWidget(self): + '''Show the login widget.''' + self._login_overlay = ftrack_connect.ui.widget.overlay.CancelOverlay( + self.loginWidget, + message='Signing in' + ) + + self._login_overlay.hide() + self.setCentralWidget(self.loginWidget) + self.loginWidget.login.connect(self._login_overlay.show) + self.loginWidget.login.connect(self.loginWithCredentials) + self.loginError.connect(self.loginWidget.loginError.emit) + self.loginError.connect(self._login_overlay.hide) + self.focus() + + # Set focus on the login widget to remove any focus from its child + # widgets. + self.loginWidget.setFocus() + self._login_overlay.hide() + + def _setup_session(self): + '''Setup a new python API session.''' + if hasattr(self, '_hub_thread'): + self._hub_thread.quit() + + plugin_paths = os.environ.get( + 'FTRACK_EVENT_PLUGIN_PATH', '' + ).split(os.pathsep) + + plugin_paths.extend(self.pluginHookPaths) + + try: + session = ftrack_connect.session.get_shared_session( + plugin_paths=plugin_paths + ) + except Exception as error: + raise ftrack_connect.error.ParseError(error) + + # Listen to events using the new API event hub. This is required to + # allow reconfiguring the storage scenario. + self._hub_thread = _event_hub_thread.NewApiEventHubThread() + self._hub_thread.start(session) + + ftrack_api._centralized_storage_scenario.register_configuration( + session + ) + + return session + + def _report_session_setup_error(self, error): + '''Format error message and emit loginError.''' + msg = ( + u'\nAn error occured while starting ftrack-connect: {0}.' + u'\nPlease check log file for more informations.' + u'\nIf the error persists please send the log file to:' + u' support@ftrack.com'.format(error) + + ) + self.loginError.emit(msg) + + def loginWithCredentials(self, url, username, apiKey): + '''Connect to *url* with *username* and *apiKey*. + + loginError will be emitted if this fails. + + ''' + # Strip all leading and preceeding occurances of slash and space. + url = url.strip('/ ') + + if not url: + self.loginError.emit( + '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: + self.logger.exception('Error reaching server url.') + self.loginError.emit( + 'The server URL you provided could not be reached.' + ) + return + + if ( + result.status_code != 200 or 'FTRACK_VERSION' not in result.headers + ): + self.loginError.emit( + '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) + return + + # Set environment variables supported by the old API. + os.environ['FTRACK_SERVER'] = url + os.environ['LOGNAME'] = username + os.environ['FTRACK_APIKEY'] = apiKey + + # Set environment variables supported by the new API. + os.environ['FTRACK_API_USER'] = username + os.environ['FTRACK_API_KEY'] = apiKey + + # Login using the new ftrack API. + try: + self._session = self._setup_session() + except Exception as error: + self.logger.exception(u'Error during login.:') + self._report_session_setup_error(error) + return + + # Store credentials since login was successful. + self._save_credentials(url, username, apiKey) + + # Verify storage scenario before starting. + if 'storage_scenario' in self._session.server_information: + storage_scenario = self._session.server_information.get( + 'storage_scenario' + ) + if storage_scenario is None: + # Hide login overlay at this time since it will be deleted + self.logger.debug('Storage scenario is not configured.') + scenario_widget = _scenario_widget.ConfigureScenario( + self._session + ) + + self.setCentralWidget(scenario_widget) + self.focus() + return diff --git a/pype/ftrack/Login/login_tools.py b/pype/ftrack/Login/login_tools.py new file mode 100644 index 0000000000..e7a35ff97f --- /dev/null +++ b/pype/ftrack/Login/login_tools.py @@ -0,0 +1,107 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2016 ftrack + +from http.server import BaseHTTPRequestHandler, HTTPServer +# import BaseHTTPServer +from urllib import parse +# import urlparse +import webbrowser +import functools +# from QtExt import QtCore +from PyQt5 import QtCore + +# class LoginServerHandler(BaseHTTPServer.BaseHTTPRequestHandler): +class LoginServerHandler(BaseHTTPRequestHandler): + '''Login server handler.''' + + def __init__(self, login_callback, *args, **kw): + '''Initialise handler.''' + self.login_callback = login_callback + BaseHTTPRequestHandler.__init__(self, *args, **kw) + + def do_GET(self): + '''Override to handle requests ourselves.''' + parsed_path = parse.urlparse(self.path) + query = parsed_path.query + + api_user = None + api_key = None + if 'api_user' and 'api_key' in query: + login_credentials = parse.parse_qs(query) + api_user = login_credentials['api_user'][0] + api_key = login_credentials['api_key'][0] + message = """ + + + +

Sign in to ftrack connect was successful

+

+ You signed in with username {0} and can now + close this window. +

+ + + """.format(api_user) + else: + message = '

Failed to sign in

' + + self.send_response(200) + self.end_headers() + self.wfile.write(message.encode()) + + if login_credentials: + self.login_callback( + api_user, + api_key + ) + + +class LoginServerThread(QtCore.QThread): + '''Login server thread.''' + + # Login signal. + # loginSignal = QtCore.Signal(object, object, object) + loginSignal = QtCore.pyqtSignal(object, object, object) + + def start(self, url): + '''Start thread.''' + self.url = url + super(LoginServerThread, self).start() + + def _handle_login(self, api_user, api_key): + '''Login to server with *api_user* and *api_key*.''' + self.loginSignal.emit(self.url, api_user, api_key) + + def run(self): + '''Listen for events.''' + # self._server = BaseHTTPServer.HTTPServer( + self._server = HTTPServer( + ('localhost', 0), + functools.partial( + LoginServerHandler, self._handle_login + ) + ) + webbrowser.open_new_tab( + '{0}/user/api_credentials?redirect_url=http://localhost:{1}'.format( + self.url, self._server.server_port + ) + ) + self._server.handle_request() diff --git a/pype/ftrack/Login/templatesforcode/login_dialogue.py b/pype/ftrack/Login/templatesforcode/login_dialogue.py new file mode 100644 index 0000000000..ba0315cabf --- /dev/null +++ b/pype/ftrack/Login/templatesforcode/login_dialogue.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file '..\CODE\github\pypeclub\pype-setup\temp\pype_project_settins_ui\login_dialogue.ui' +# +# Created by: PyQt5 UI code generator 5.7.1 +# +# WARNING! All changes made in this file will be lost! + +import sys +from PyQt5 import QtCore, QtGui, QtWidgets + +from app import style + + + + +class Login_Dialog_ui(object): + + SIZE_W = 250 + SIZE_H = 300 + + def __init__(self): + super(Login_Dialog_ui, self).__init__() + self.Dialog = QtWidgets.QDialog() + self.Dialog.setStyleSheet(style.load_stylesheet()) + self.Dialog.setObjectName("Dialog") + self.Dialog.resize(self.SIZE_W, self.SIZE_H) + self.Dialog.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) + self.verticalLayoutWidget = QtWidgets.QWidget(self.Dialog) + self.verticalLayoutWidget.setGeometry(QtCore.QRect(0, 0, self.SIZE_W + 1, self.SIZE_H + 1)) + self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget) + self.verticalLayout.setContentsMargins(10, 5, 10, 5) + self.verticalLayout.setObjectName("verticalLayout") + + font = QtGui.QFont() + font.setFamily("DejaVu Sans Condensed") + font.setPointSize(9) + font.setBold(True) + font.setWeight(50) + font.setKerning(True) + + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + + if self.ftracksite: + self.site_label = QtWidgets.QLabel(self.verticalLayoutWidget) + sizePolicy.setHeightForWidth(self.site_label.sizePolicy().hasHeightForWidth()) + self.site_label.setSizePolicy(sizePolicy) + self.site_label.setMinimumSize(QtCore.QSize(150, 28)) + self.site_label.setFont(font) + self.site_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.site_label.setTextFormat(QtCore.Qt.RichText) + # self.site_label.setAlignment(QtCore.Qt.AlignCenter) + self.site_label.setObjectName("site_label") + self.verticalLayout.addWidget(self.site_label) + + self.site_input = QtWidgets.QLineEdit(self.verticalLayoutWidget) + self.site_input.setEnabled(True) + self.site_input.setFrame(True) + self.site_input.setFrame(True) + self.site_input.setReadOnly(True) + self.site_input.setObjectName("site_input") + self.verticalLayout.addWidget(self.site_input) + + self.user_label = QtWidgets.QLabel(self.verticalLayoutWidget) + sizePolicy.setHeightForWidth(self.user_label.sizePolicy().hasHeightForWidth()) + self.user_label.setSizePolicy(sizePolicy) + self.user_label.setMinimumSize(QtCore.QSize(150, 28)) + self.user_label.setFont(font) + self.user_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.user_label.setTextFormat(QtCore.Qt.RichText) + # self.user_label.setAlignment(QtCore.Qt.AlignCenter) + self.user_label.setObjectName("user_label") + self.verticalLayout.addWidget(self.user_label) + + self.user_input = QtWidgets.QLineEdit(self.verticalLayoutWidget) + self.user_input.setEnabled(True) + self.user_input.setFrame(True) + self.user_input.setObjectName("user_input") + self.verticalLayout.addWidget(self.user_input) + + self.api_label = QtWidgets.QLabel(self.verticalLayoutWidget) + sizePolicy.setHeightForWidth(self.api_label.sizePolicy().hasHeightForWidth()) + self.api_label.setSizePolicy(sizePolicy) + self.api_label.setMinimumSize(QtCore.QSize(150, 28)) + self.api_label.setFont(font) + self.api_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.api_label.setTextFormat(QtCore.Qt.RichText) + # self.api_label.setAlignment(QtCore.Qt.AlignCenter) + self.api_label.setObjectName("api_label") + self.verticalLayout.addWidget(self.api_label) + + self.api_input = QtWidgets.QLineEdit(self.verticalLayoutWidget) + self.api_input.setEnabled(True) + self.api_input.setFrame(True) + self.api_input.setObjectName("api_input") + self.verticalLayout.addWidget(self.api_input) + + spacerItem = QtWidgets.QSpacerItem( + 20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + + + # button.setToolTip('This is an example button') + # button.move(100,70) + self.btn_ftrack = QtWidgets.QPushButton("Login", self.verticalLayoutWidget) + self.btn_ftrack.resize(10,10) + # self.btn_ftrack.move(100,70) + self.verticalLayout.addWidget(self.btn_ftrack) + + self.buttonBox = QtWidgets.QDialogButtonBox(self.verticalLayoutWidget) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons( + QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout.addWidget(self.buttonBox) + + self.retranslateUi(self.Dialog) + self.buttonBox.accepted.connect(self.execute) + self.buttonBox.rejected.connect(self.Dialog.reject) + QtCore.QMetaObject.connectSlotsByName(self.Dialog) + self.Dialog.setTabOrder(self.user_input, self.api_input) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", self.ui_title)) + self.site_label.setText(_translate("Dialog", "FTrack URL:")) + if self.ftracksite: + self.site_input.setText(_translate("Dialog", self.ftracksite)) + self.user_label.setText(_translate("Dialog", "Username:")) + self.user_input.setPlaceholderText(_translate("Dialog", "user.name")) + self.api_label.setText(_translate("Dialog", "API Key:")) + self.api_input.setPlaceholderText(_translate("Dialog", "eg.:")) + + def show(self): + self.Dialog.show() + + +class Login_Dialog(Login_Dialog_ui): + def __init__(self, ui_title="Dialog", ftracksite=None): + self.ui_title = ui_title + self.ftracksite = ftracksite + super(Login_Dialog, self).__init__() + self.user_input.textChanged.connect(self._user_changed) + self.api_input.textChanged.connect(self._api_changed) + + def _user_changed(self): + self.user_input.setStyleSheet("") + + def _api_changed(self): + self.api_input.setStyleSheet("") + # print(self.passw_input.text()) + + def _invalid_input(self,entity): + entity.setStyleSheet("border: 1px solid red;") + + def _check_credentials(self): + logged = False + + user = self.user_input.text() + api = self.api_input.text() + + if user == "": + self._invalid_input(self.user_input) + elif True: + # IF user exist + pass + + if api == "": + self._invalid_input(self.api_input) + elif True: + # IF is api ok exist - Session creation + pass + + if logged is True: + self.close() + + def execute(self): + self._check_credentials() + + +def getApp(): + return QtWidgets.QApplication(sys.argv) + +def main(): + app = QtWidgets.QApplication(sys.argv) + ui = Login_Dialog("Ftrack Login","pype") + ui.show() + sys.exit(app.exec_()) + + +if __name__ == "__main__": + main() + +main() diff --git a/pype/ftrack/Login/templatesforcode/login_dialogue_usage.py b/pype/ftrack/Login/templatesforcode/login_dialogue_usage.py new file mode 100644 index 0000000000..ac6e986bef --- /dev/null +++ b/pype/ftrack/Login/templatesforcode/login_dialogue_usage.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file '..\CODE\github\pypeclub\pype-setup\temp\pype_project_settins_ui\login_dialogue.ui' +# +# Created by: PyQt5 UI code generator 5.7.1 +# +# WARNING! All changes made in this file will be lost! + +import sys +from PyQt5 import QtCore, QtGui, QtWidgets + +from app import style + + +class Login_Dialog_ui(object): + + SIZE_W = 250 + SIZE_H = 300 + + def __init__(self): + super(Login_Dialog_ui, self).__init__() + self.Dialog = QtWidgets.QDialog() + self.Dialog.setStyleSheet(style.load_stylesheet()) + self.Dialog.setObjectName("Dialog") + self.Dialog.resize(self.SIZE_W, self.SIZE_H) + self.Dialog.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) + self.verticalLayoutWidget = QtWidgets.QWidget(self.Dialog) + self.verticalLayoutWidget.setGeometry(QtCore.QRect(0, 0, self.SIZE_W + 1, self.SIZE_H + 1)) + self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget) + self.verticalLayout.setContentsMargins(10, 5, 10, 5) + self.verticalLayout.setObjectName("verticalLayout") + + font = QtGui.QFont() + font.setFamily("DejaVu Sans Condensed") + font.setPointSize(9) + font.setBold(True) + font.setWeight(50) + font.setKerning(True) + + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + + if self.ftracksite: + self.site_label = QtWidgets.QLabel(self.verticalLayoutWidget) + sizePolicy.setHeightForWidth(self.site_label.sizePolicy().hasHeightForWidth()) + self.site_label.setSizePolicy(sizePolicy) + self.site_label.setMinimumSize(QtCore.QSize(150, 28)) + self.site_label.setFont(font) + self.site_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.site_label.setTextFormat(QtCore.Qt.RichText) + # self.site_label.setAlignment(QtCore.Qt.AlignCenter) + self.site_label.setObjectName("site_label") + self.verticalLayout.addWidget(self.site_label) + + self.site_input = QtWidgets.QLineEdit(self.verticalLayoutWidget) + self.site_input.setEnabled(True) + self.site_input.setFrame(True) + self.site_input.setFrame(True) + self.site_input.setReadOnly(True) + self.site_input.setObjectName("site_input") + self.verticalLayout.addWidget(self.site_input) + + self.user_label = QtWidgets.QLabel(self.verticalLayoutWidget) + sizePolicy.setHeightForWidth(self.user_label.sizePolicy().hasHeightForWidth()) + self.user_label.setSizePolicy(sizePolicy) + self.user_label.setMinimumSize(QtCore.QSize(150, 28)) + self.user_label.setFont(font) + self.user_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.user_label.setTextFormat(QtCore.Qt.RichText) + # self.user_label.setAlignment(QtCore.Qt.AlignCenter) + self.user_label.setObjectName("user_label") + self.verticalLayout.addWidget(self.user_label) + + self.user_input = QtWidgets.QLineEdit(self.verticalLayoutWidget) + self.user_input.setEnabled(True) + self.user_input.setFrame(True) + self.user_input.setObjectName("user_input") + self.verticalLayout.addWidget(self.user_input) + + self.api_label = QtWidgets.QLabel(self.verticalLayoutWidget) + sizePolicy.setHeightForWidth(self.api_label.sizePolicy().hasHeightForWidth()) + self.api_label.setSizePolicy(sizePolicy) + self.api_label.setMinimumSize(QtCore.QSize(150, 28)) + self.api_label.setFont(font) + self.api_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.api_label.setTextFormat(QtCore.Qt.RichText) + # self.api_label.setAlignment(QtCore.Qt.AlignCenter) + self.api_label.setObjectName("api_label") + self.verticalLayout.addWidget(self.api_label) + + self.api_input = QtWidgets.QLineEdit(self.verticalLayoutWidget) + self.api_input.setEnabled(True) + self.api_input.setFrame(True) + self.api_input.setObjectName("api_input") + self.verticalLayout.addWidget(self.api_input) + + spacerItem = QtWidgets.QSpacerItem( + 20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + + + # button.setToolTip('This is an example button') + # button.move(100,70) + self.btn_ftrack = QtWidgets.QPushButton("Login", self.verticalLayoutWidget) + self.btn_ftrack.resize(10,10) + # self.btn_ftrack.move(100,70) + self.verticalLayout.addWidget(self.btn_ftrack) + + self.buttonBox = QtWidgets.QDialogButtonBox(self.verticalLayoutWidget) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons( + QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout.addWidget(self.buttonBox) + + self.retranslateUi(self.Dialog) + self.buttonBox.accepted.connect(self.execute) + self.buttonBox.rejected.connect(self.Dialog.reject) + QtCore.QMetaObject.connectSlotsByName(self.Dialog) + self.Dialog.setTabOrder(self.user_input, self.api_input) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", self.ui_title)) + self.site_label.setText(_translate("Dialog", "FTrack URL:")) + if self.ftracksite: + self.site_input.setText(_translate("Dialog", self.ftracksite)) + self.user_label.setText(_translate("Dialog", "Username:")) + self.user_input.setPlaceholderText(_translate("Dialog", "user.name")) + self.api_label.setText(_translate("Dialog", "API Key:")) + self.api_input.setPlaceholderText(_translate("Dialog", "eg.:")) + + def show(self): + self.Dialog.show() + + +class Login_Dialog(Login_Dialog_ui): + def __init__(self, ui_title="Dialog", ftracksite=None): + self.ui_title = ui_title + self.ftracksite = ftracksite + super(Login_Dialog, self).__init__() + self.user_input.textChanged.connect(self._user_changed) + self.api_input.textChanged.connect(self._api_changed) + + def _user_changed(self): + self.user_input.setStyleSheet("") + + def _api_changed(self): + self.api_input.setStyleSheet("") + # print(self.passw_input.text()) + + def _invalid_input(self,entity): + entity.setStyleSheet("border: 1px solid red;") + + def _check_credentials(self): + logged = False + + user = self.user_input.text() + api = self.api_input.text() + + if user == "": + self._invalid_input(self.user_input) + elif True: + # IF user exist + pass + + if api == "": + self._invalid_input(self.api_input) + elif True: + # IF is api ok exist - Session creation + pass + + if logged is True: + self.close() + + def execute(self): + self._check_credentials() + + +def getApp(): + return QtWidgets.QApplication(sys.argv) + +def main(): + app = getApp() + ui = Login_Dialog("Ftrack Login","pype") + ui.show() + sys.exit(app.exec_()) + + +if __name__ == "__main__": + main() + +main() diff --git a/pype/ftrack/__init__.py b/pype/ftrack/__init__.py index e69de29bb2..bf220994fd 100644 --- a/pype/ftrack/__init__.py +++ b/pype/ftrack/__init__.py @@ -0,0 +1 @@ +import Login From aaacaf88c4fbc19eb8c628f131737bdb4b69166a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 9 Nov 2018 15:45:10 +0100 Subject: [PATCH 08/16] It looks like login is working. Added module "appsdir" --- pype/ftrack/Login/__init__.py | 6 - pype/ftrack/Login/login_dialog.py | 168 ----- .../{ => templatesforcode}/login_setup.py | 0 pype/ftrack/__init__.py | 16 +- pype/ftrack/{Login => }/credentials.py | 25 +- pype/ftrack/login_dialog.py | 310 +++++++++ pype/ftrack/{Login => }/login_tools.py | 4 +- pype/vendor/appdirs.py | 608 ++++++++++++++++++ 8 files changed, 954 insertions(+), 183 deletions(-) delete mode 100644 pype/ftrack/Login/__init__.py delete mode 100644 pype/ftrack/Login/login_dialog.py rename pype/ftrack/Login/{ => templatesforcode}/login_setup.py (100%) rename pype/ftrack/{Login => }/credentials.py (63%) create mode 100644 pype/ftrack/login_dialog.py rename pype/ftrack/{Login => }/login_tools.py (98%) create mode 100644 pype/vendor/appdirs.py diff --git a/pype/ftrack/Login/__init__.py b/pype/ftrack/Login/__init__.py deleted file mode 100644 index 80bdb7f70f..0000000000 --- a/pype/ftrack/Login/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -import os -import sys -import credentials -import login_dialog - -# login_dialogue_usage.main() diff --git a/pype/ftrack/Login/login_dialog.py b/pype/ftrack/Login/login_dialog.py deleted file mode 100644 index a7186ab415..0000000000 --- a/pype/ftrack/Login/login_dialog.py +++ /dev/null @@ -1,168 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file '..\CODE\github\pypeclub\pype-setup\temp\pype_project_settins_ui\login_dialogue.ui' -# -# Created by: PyQt5 UI code generator 5.7.1 -# -# WARNING! All changes made in this file will be lost! - -import sys -from PyQt5 import QtCore, QtGui, QtWidgets -from app import style -import credentials -import login_tools - -class Login_Dialog_ui(QtWidgets.QWidget): - - SIZE_W = 300 - SIZE_H = 160 - - def __init__(self): - super().__init__() - - _translate = QtCore.QCoreApplication.translate - - self.resize(self.SIZE_W, self.SIZE_H) - self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) - self.setStyleSheet(style.load_stylesheet()) - - self.main = QtWidgets.QVBoxLayout() - self.main.setObjectName("main") - - self.form = QtWidgets.QFormLayout() - self.form.setContentsMargins(10, 15, 10, 5) - self.form.setObjectName("form") - - font = QtGui.QFont() - font.setFamily("DejaVu Sans Condensed") - font.setPointSize(9) - font.setBold(True) - font.setWeight(50) - font.setKerning(True) - - self.ftsite_label = QtWidgets.QLabel("FTrack URL:") - self.ftsite_label.setFont(font) - self.ftsite_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.ftsite_label.setTextFormat(QtCore.Qt.RichText) - self.ftsite_label.setObjectName("user_label") - - self.ftsite_input = QtWidgets.QLineEdit() - self.ftsite_input.setEnabled(True) - self.ftsite_input.setFrame(True) - self.ftsite_input.setEnabled(False) - self.ftsite_input.setReadOnly(True) - self.ftsite_input.setObjectName("ftsite_input") - - self.user_label = QtWidgets.QLabel("Username:") - self.user_label.setFont(font) - self.user_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.user_label.setTextFormat(QtCore.Qt.RichText) - self.user_label.setObjectName("user_label") - - self.user_input = QtWidgets.QLineEdit() - self.user_input.setEnabled(True) - self.user_input.setFrame(True) - self.user_input.setObjectName("user_input") - self.user_input.setPlaceholderText(_translate("main","user.name")) - - self.api_label = QtWidgets.QLabel("API Key:") - self.api_label.setFont(font) - self.api_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.api_label.setTextFormat(QtCore.Qt.RichText) - self.api_label.setObjectName("api_label") - - self.api_input = QtWidgets.QLineEdit() - self.api_input.setEnabled(True) - self.api_input.setFrame(True) - self.api_input.setObjectName("api_input") - self.api_input.setPlaceholderText(_translate("main","e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")) - - self.form.addRow(self.ftsite_label, self.ftsite_input) - self.form.addRow(self.user_label, self.user_input) - self.form.addRow(self.api_label,self.api_input) - - self.btnGroup = QtWidgets.QHBoxLayout() - self.btnGroup.addStretch(1) - self.btnGroup.setObjectName("btnGroup") - - self.btnEnter = QtWidgets.QPushButton("Login") - self.btnEnter.setToolTip('Set Username and API Key with entered values') - self.btnEnter.clicked.connect(self._enter_credentials) - - self.btnClose = QtWidgets.QPushButton("Close") - self.btnClose.setToolTip('Close this window') - self.btnClose.clicked.connect(self._close_widget) - - self.btnFtrack = QtWidgets.QPushButton("Ftrack") - self.btnFtrack.setToolTip('Open browser for Login to Ftrack') - self.btnFtrack.clicked.connect(self._open_ftrack) - - self.btnGroup.addWidget(self.btnFtrack) - self.btnGroup.addWidget(self.btnEnter) - self.btnGroup.addWidget(self.btnClose) - - self.main.addLayout(self.form) - self.main.addLayout(self.btnGroup) - - self.setLayout(self.main) - self.setWindowTitle('FTrack Login') - self._set_site() - self.show() - - def _set_site(self): - try: - txt = os.getenv('FTRACK_SERVER') - except: - txt = "FTrack site si is not set!" - - self.ftsite_input.setText(txt) - - def _enter_credentials(self): - print("EnteredCredentials!") - user = self.user_input.text() - api = self.api_input.text() - verification = credentials._check_credentials(user, api) - - if verification: - print("SUCCESS") - credentials._save_credentials(user, api) - credentials._set_env(user, api) - self._close_widget() - - def _open_ftrack(self): - print("OpenWindow!") - try: - url = "pype.ftrackapp.com" - self.loginSignal = QtCore.pyqtSignal(object, object, object) - self._login_server_thread = login_tools.LoginServerThread() - self._login_server_thread.loginSignal.connect(self.loginSignal) - self._login_server_thread.start(url) - except Exception as e: - print(e) - - def _close_widget(self): - sys.exit(app.exec_()) - - -class Login_Dialog(Login_Dialog_ui): - def __init__(self): - super(Login_Dialog, self).__init__() - - def execute(self): - self._check_credentials() - - -def getApp(): - return QtWidgets.QApplication(sys.argv) - -def main(): - app = getApp() - ui = Login_Dialog() - ui.show() - sys.exit(app.exec_()) - - -if __name__ == "__main__": - main() - -main() diff --git a/pype/ftrack/Login/login_setup.py b/pype/ftrack/Login/templatesforcode/login_setup.py similarity index 100% rename from pype/ftrack/Login/login_setup.py rename to pype/ftrack/Login/templatesforcode/login_setup.py diff --git a/pype/ftrack/__init__.py b/pype/ftrack/__init__.py index bf220994fd..44f9b76bcc 100644 --- a/pype/ftrack/__init__.py +++ b/pype/ftrack/__init__.py @@ -1 +1,15 @@ -import Login +import credentials +import login_dialog + +cred = credentials._get_credentials() + +if 'username' in cred and 'apiKey' in cred: + validation = credentials._check_credentials( + cred['username'], + cred['apiKey'] + ) + if validation is False: + login_dialog.run_login() + +else: + login_dialog.run_login() diff --git a/pype/ftrack/Login/credentials.py b/pype/ftrack/credentials.py similarity index 63% rename from pype/ftrack/Login/credentials.py rename to pype/ftrack/credentials.py index 9f409c5349..46292ebee7 100644 --- a/pype/ftrack/Login/credentials.py +++ b/pype/ftrack/credentials.py @@ -1,17 +1,25 @@ import os import toml -# import ftrack_api +import ftrack_api +import appdirs -# TODO JUST TEST PATH - path should be in Environment Variables... -config_path = r"C:\test" +config_path = os.path.normpath(appdirs.user_data_dir('pype-app','pype')) config_name = 'credentials.toml' fpath = os.path.join(config_path, config_name) def _get_credentials(): + + folder = os.path.dirname(fpath) + + if not os.path.isdir(folder): + os.makedirs(folder) + try: file = open(fpath, 'r') except: - file = open(fpath, 'w') + filecreate = open(fpath, 'w') + filecreate.close() + file = open(fpath, 'r') credentials = toml.load(file) file.close() @@ -37,13 +45,16 @@ def _set_env(username, apiKey): os.environ['FTRACK_API_USER'] = username os.environ['FTRACK_API_KEY'] = apiKey -def _check_credentials(username, apiKey): +def _check_credentials(username=None, apiKey=None): - _set_env(username, apiKey) + if username and apiKey: + _set_env(username, apiKey) try: session = ftrack_api.Session() - return True except Exception as e: print(e) return False + + session.close() + return True diff --git a/pype/ftrack/login_dialog.py b/pype/ftrack/login_dialog.py new file mode 100644 index 0000000000..0e671c7ffd --- /dev/null +++ b/pype/ftrack/login_dialog.py @@ -0,0 +1,310 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file '..\CODE\github\pypeclub\pype-setup\temp\pype_project_settins_ui\login_dialogue.ui' +# +# Created by: PyQt5 UI code generator 5.7.1 +# +# WARNING! All changes made in this file will be lost! + +import sys +from PyQt5 import QtCore, QtGui, QtWidgets +from app import style +import credentials +import login_tools +import requests +class Login_Dialog_ui(QtWidgets.QWidget): + + SIZE_W = 300 + SIZE_H = 230 + + loginSignal = QtCore.pyqtSignal(object, object, object) + _login_server_thread = None + inputs = [] + buttons = [] + labels = [] + + def __init__(self): + + super().__init__() + self.loginSignal.connect(self.loginWithCredentials) + self._translate = QtCore.QCoreApplication.translate + + self.font = QtGui.QFont() + self.font.setFamily("DejaVu Sans Condensed") + self.font.setPointSize(9) + self.font.setBold(True) + self.font.setWeight(50) + self.font.setKerning(True) + + self.resize(self.SIZE_W, self.SIZE_H) + self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) + self.setStyleSheet(style.load_stylesheet()) + + self.setLayout(self._main()) + self.setWindowTitle('FTrack Login') + + self.show() + + def _main(self): + self.main = QtWidgets.QVBoxLayout() + self.main.setObjectName("main") + + self.form = QtWidgets.QFormLayout() + self.form.setContentsMargins(10, 15, 10, 5) + self.form.setObjectName("form") + + self.ftsite_label = QtWidgets.QLabel("FTrack URL:") + self.ftsite_label.setFont(self.font) + self.ftsite_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.ftsite_label.setTextFormat(QtCore.Qt.RichText) + self.ftsite_label.setObjectName("user_label") + + self.ftsite_input = QtWidgets.QLineEdit() + self.ftsite_input.setEnabled(True) + self.ftsite_input.setFrame(True) + self.ftsite_input.setEnabled(False) + self.ftsite_input.setReadOnly(True) + self.ftsite_input.setObjectName("ftsite_input") + + self.user_label = QtWidgets.QLabel("Username:") + self.user_label.setFont(self.font) + self.user_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.user_label.setTextFormat(QtCore.Qt.RichText) + self.user_label.setObjectName("user_label") + + self.user_input = QtWidgets.QLineEdit() + self.user_input.setEnabled(True) + self.user_input.setFrame(True) + self.user_input.setObjectName("user_input") + self.user_input.setPlaceholderText(self._translate("main","user.name")) + self.user_input.textChanged.connect(self._user_changed) + + self.api_label = QtWidgets.QLabel("API Key:") + self.api_label.setFont(self.font) + self.api_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.api_label.setTextFormat(QtCore.Qt.RichText) + self.api_label.setObjectName("api_label") + + self.api_input = QtWidgets.QLineEdit() + self.api_input.setEnabled(True) + self.api_input.setFrame(True) + self.api_input.setObjectName("api_input") + self.api_input.setPlaceholderText(self._translate("main","e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")) + self.api_input.textChanged.connect(self._api_changed) + + self.error_label = QtWidgets.QLabel("") + self.error_label.setFont(self.font) + self.error_label.setTextFormat(QtCore.Qt.RichText) + self.error_label.setObjectName("error_label") + self.error_label.setWordWrap(True); + self.error_label.hide() + + self.form.addRow(self.ftsite_label, self.ftsite_input) + self.form.addRow(self.user_label, self.user_input) + self.form.addRow(self.api_label,self.api_input) + self.form.addRow(self.error_label) + + self.btnGroup = QtWidgets.QHBoxLayout() + self.btnGroup.addStretch(1) + self.btnGroup.setObjectName("btnGroup") + + self.btnEnter = QtWidgets.QPushButton("Login") + self.btnEnter.setToolTip('Set Username and API Key with entered values') + self.btnEnter.clicked.connect(self.enter_credentials) + + self.btnClose = QtWidgets.QPushButton("Close") + self.btnClose.setToolTip('Close this window') + self.btnClose.clicked.connect(self._close_widget) + + self.btnFtrack = QtWidgets.QPushButton("Ftrack") + self.btnFtrack.setToolTip('Open browser for Login to Ftrack') + self.btnFtrack.clicked.connect(self.open_ftrack) + + self.btnGroup.addWidget(self.btnFtrack) + self.btnGroup.addWidget(self.btnEnter) + self.btnGroup.addWidget(self.btnClose) + + self.main.addLayout(self.form) + self.main.addLayout(self.btnGroup) + + self.inputs.append(self.api_input) + self.inputs.append(self.user_input) + self.inputs.append(self.ftsite_input) + + self.enter_site() + return self.main + + def enter_site(self): + try: + # # TESTING + # url = ".ftrackapp.com" + url = os.getenv('FTRACK_SERVER') + newurl = self.checkUrl(url) + + if newurl is None: + self.btnEnter.setEnabled(False) + self.btnFtrack.setEnabled(False) + for input in self.inputs: + input.setEnabled(False) + newurl = url + + self.ftsite_input.setText(newurl) + + except: + self.setError("FTRACK_SERVER is not set in templates") + self.btnEnter.setEnabled(False) + self.btnFtrack.setEnabled(False) + for input in self.inputs: + input.setEnabled(False) + + def setError(self, msg): + self.error_label.setText(msg) + self.error_label.show() + + def _user_changed(self): + self.user_input.setStyleSheet("") + + def _api_changed(self): + self.api_input.setStyleSheet("") + + def _invalid_input(self,entity): + entity.setStyleSheet("border: 1px solid red;") + + def enter_credentials(self): + user = self.user_input.text().strip() + api = self.api_input.text().strip() + msg = "You didn't enter " + missing = [] + if user == "": + missing.append("Username") + self._invalid_input(self.user_input) + + if api == "": + missing.append("API Key") + self._invalid_input(self.api_input) + + if len(missing) > 0: + self.setError("{0} {1}".format(msg, " and ".join(missing))) + return + + verification = credentials._check_credentials(user, api) + + if verification: + credentials._save_credentials(username, apiKey) + credentials._set_env(username, apiKey) + self._close_widget() + else: + self._invalid_input(self.user_input) + self._invalid_input(self.api_input) + self.setError("We're unable to connect to Ftrack with these credentials") + + def open_ftrack(self): + url = self.ftsite_input.text() + self.loginWithCredentials(url,None,None) + + def checkUrl(self, url): + url = url.strip('/ ') + + if not url: + self.setError() + 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: + self.setError( + 'The server URL set in Templates could not be reached.' + ) + return + + + if ( + result.status_code != 200 or 'FTRACK_VERSION' not in result.headers + ): + self.setError( + '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: + self.setError( + '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: + self.setError( + 'The server URL you provided could not be reached.' + ) + return + + + if ( + result.status_code != 200 or 'FTRACK_VERSION' not in result.headers + ): + self.setError( + '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) + return + + verification = credentials._check_credentials(user, api) + + if verification is True: + credentials._save_credentials(username, apiKey) + credentials._set_env(username, apiKey) + self._close_widget() + + + def _close_widget(self): + sys.exit(app.exec_()) + + +class Login_Dialog(Login_Dialog_ui): + def __init__(self): + super(Login_Dialog, self).__init__() + + +def getApp(): + return QtWidgets.QApplication(sys.argv) + +def run_login(): + app = getApp() + ui = Login_Dialog() + ui.show() + sys.exit(app.exec_()) diff --git a/pype/ftrack/Login/login_tools.py b/pype/ftrack/login_tools.py similarity index 98% rename from pype/ftrack/Login/login_tools.py rename to pype/ftrack/login_tools.py index e7a35ff97f..200638b3df 100644 --- a/pype/ftrack/Login/login_tools.py +++ b/pype/ftrack/login_tools.py @@ -26,6 +26,7 @@ class LoginServerHandler(BaseHTTPRequestHandler): api_user = None api_key = None + login_credentials = None if 'api_user' and 'api_key' in query: login_credentials = parse.parse_qs(query) api_user = login_credentials['api_user'][0] @@ -67,6 +68,7 @@ class LoginServerHandler(BaseHTTPRequestHandler): self.end_headers() self.wfile.write(message.encode()) + if login_credentials: self.login_callback( api_user, @@ -78,9 +80,9 @@ class LoginServerThread(QtCore.QThread): '''Login server thread.''' # Login signal. - # loginSignal = QtCore.Signal(object, object, object) loginSignal = QtCore.pyqtSignal(object, object, object) + def start(self, url): '''Start thread.''' self.url = url diff --git a/pype/vendor/appdirs.py b/pype/vendor/appdirs.py new file mode 100644 index 0000000000..ae67001af8 --- /dev/null +++ b/pype/vendor/appdirs.py @@ -0,0 +1,608 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2005-2010 ActiveState Software Inc. +# Copyright (c) 2013 Eddy Petrișor + +"""Utilities for determining application-specific dirs. + +See for details and usage. +""" +# Dev Notes: +# - MSDN on where to store app data files: +# http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120 +# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html +# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + +__version_info__ = (1, 4, 3) +__version__ = '.'.join(map(str, __version_info__)) + + +import sys +import os + +PY3 = sys.version_info[0] == 3 + +if PY3: + unicode = str + +if sys.platform.startswith('java'): + import platform + os_name = platform.java_ver()[3][0] + if os_name.startswith('Windows'): # "Windows XP", "Windows 7", etc. + system = 'win32' + elif os_name.startswith('Mac'): # "Mac OS X", etc. + system = 'darwin' + else: # "Linux", "SunOS", "FreeBSD", etc. + # Setting this to "linux2" is not ideal, but only Windows or Mac + # are actually checked for and the rest of the module expects + # *sys.platform* style strings. + system = 'linux2' +else: + system = sys.platform + + + +def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user data directories are: + Mac OS X: ~/Library/Application Support/ + Unix: ~/.local/share/ # or in $XDG_DATA_HOME, if defined + Win XP (not roaming): C:\Documents and Settings\\Application Data\\ + Win XP (roaming): C:\Documents and Settings\\Local Settings\Application Data\\ + Win 7 (not roaming): C:\Users\\AppData\Local\\ + Win 7 (roaming): C:\Users\\AppData\Roaming\\ + + For Unix, we follow the XDG spec and support $XDG_DATA_HOME. + That means, by default "~/.local/share/". + """ + if system == "win32": + if appauthor is None: + appauthor = appname + const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA" + path = os.path.normpath(_get_win_folder(const)) + if appname: + if appauthor is not False: + path = os.path.join(path, appauthor, appname) + else: + path = os.path.join(path, appname) + elif system == 'darwin': + path = os.path.expanduser('~/Library/Application Support/') + if appname: + path = os.path.join(path, appname) + else: + path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def site_data_dir(appname=None, appauthor=None, version=None, multipath=False): + r"""Return full path to the user-shared data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "multipath" is an optional parameter only applicable to *nix + which indicates that the entire list of data dirs should be + returned. By default, the first item from XDG_DATA_DIRS is + returned, or '/usr/local/share/', + if XDG_DATA_DIRS is not set + + Typical site data directories are: + Mac OS X: /Library/Application Support/ + Unix: /usr/local/share/ or /usr/share/ + Win XP: C:\Documents and Settings\All Users\Application Data\\ + Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.) + Win 7: C:\ProgramData\\ # Hidden, but writeable on Win 7. + + For Unix, this is using the $XDG_DATA_DIRS[0] default. + + WARNING: Do not use this on Windows. See the Vista-Fail note above for why. + """ + if system == "win32": + if appauthor is None: + appauthor = appname + path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA")) + if appname: + if appauthor is not False: + path = os.path.join(path, appauthor, appname) + else: + path = os.path.join(path, appname) + elif system == 'darwin': + path = os.path.expanduser('/Library/Application Support') + if appname: + path = os.path.join(path, appname) + else: + # XDG default for $XDG_DATA_DIRS + # only first, if multipath is False + path = os.getenv('XDG_DATA_DIRS', + os.pathsep.join(['/usr/local/share', '/usr/share'])) + pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)] + if appname: + if version: + appname = os.path.join(appname, version) + pathlist = [os.sep.join([x, appname]) for x in pathlist] + + if multipath: + path = os.pathsep.join(pathlist) + else: + path = pathlist[0] + return path + + if appname and version: + path = os.path.join(path, version) + return path + + +def user_config_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific config dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user config directories are: + Mac OS X: same as user_data_dir + Unix: ~/.config/ # or in $XDG_CONFIG_HOME, if defined + Win *: same as user_data_dir + + For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME. + That means, by default "~/.config/". + """ + if system in ["win32", "darwin"]: + path = user_data_dir(appname, appauthor, None, roaming) + else: + path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def site_config_dir(appname=None, appauthor=None, version=None, multipath=False): + r"""Return full path to the user-shared data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "multipath" is an optional parameter only applicable to *nix + which indicates that the entire list of config dirs should be + returned. By default, the first item from XDG_CONFIG_DIRS is + returned, or '/etc/xdg/', if XDG_CONFIG_DIRS is not set + + Typical site config directories are: + Mac OS X: same as site_data_dir + Unix: /etc/xdg/ or $XDG_CONFIG_DIRS[i]/ for each value in + $XDG_CONFIG_DIRS + Win *: same as site_data_dir + Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.) + + For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False + + WARNING: Do not use this on Windows. See the Vista-Fail note above for why. + """ + if system in ["win32", "darwin"]: + path = site_data_dir(appname, appauthor) + if appname and version: + path = os.path.join(path, version) + else: + # XDG default for $XDG_CONFIG_DIRS + # only first, if multipath is False + path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg') + pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)] + if appname: + if version: + appname = os.path.join(appname, version) + pathlist = [os.sep.join([x, appname]) for x in pathlist] + + if multipath: + path = os.pathsep.join(pathlist) + else: + path = pathlist[0] + return path + + +def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True): + r"""Return full path to the user-specific cache dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "opinion" (boolean) can be False to disable the appending of + "Cache" to the base app data dir for Windows. See + discussion below. + + Typical user cache directories are: + Mac OS X: ~/Library/Caches/ + Unix: ~/.cache/ (XDG default) + Win XP: C:\Documents and Settings\\Local Settings\Application Data\\\Cache + Vista: C:\Users\\AppData\Local\\\Cache + + On Windows the only suggestion in the MSDN docs is that local settings go in + the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming + app data dir (the default returned by `user_data_dir` above). Apps typically + put cache data somewhere *under* the given dir here. Some examples: + ...\Mozilla\Firefox\Profiles\\Cache + ...\Acme\SuperApp\Cache\1.0 + OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value. + This can be disabled with the `opinion=False` option. + """ + if system == "win32": + if appauthor is None: + appauthor = appname + path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA")) + if appname: + if appauthor is not False: + path = os.path.join(path, appauthor, appname) + else: + path = os.path.join(path, appname) + if opinion: + path = os.path.join(path, "Cache") + elif system == 'darwin': + path = os.path.expanduser('~/Library/Caches') + if appname: + path = os.path.join(path, appname) + else: + path = os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache')) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def user_state_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific state dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user state directories are: + Mac OS X: same as user_data_dir + Unix: ~/.local/state/ # or in $XDG_STATE_HOME, if defined + Win *: same as user_data_dir + + For Unix, we follow this Debian proposal + to extend the XDG spec and support $XDG_STATE_HOME. + + That means, by default "~/.local/state/". + """ + if system in ["win32", "darwin"]: + path = user_data_dir(appname, appauthor, None, roaming) + else: + path = os.getenv('XDG_STATE_HOME', os.path.expanduser("~/.local/state")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def user_log_dir(appname=None, appauthor=None, version=None, opinion=True): + r"""Return full path to the user-specific log dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "opinion" (boolean) can be False to disable the appending of + "Logs" to the base app data dir for Windows, and "log" to the + base cache dir for Unix. See discussion below. + + Typical user log directories are: + Mac OS X: ~/Library/Logs/ + Unix: ~/.cache//log # or under $XDG_CACHE_HOME if defined + Win XP: C:\Documents and Settings\\Local Settings\Application Data\\\Logs + Vista: C:\Users\\AppData\Local\\\Logs + + On Windows the only suggestion in the MSDN docs is that local settings + go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in + examples of what some windows apps use for a logs dir.) + + OPINION: This function appends "Logs" to the `CSIDL_LOCAL_APPDATA` + value for Windows and appends "log" to the user cache dir for Unix. + This can be disabled with the `opinion=False` option. + """ + if system == "darwin": + path = os.path.join( + os.path.expanduser('~/Library/Logs'), + appname) + elif system == "win32": + path = user_data_dir(appname, appauthor, version) + version = False + if opinion: + path = os.path.join(path, "Logs") + else: + path = user_cache_dir(appname, appauthor, version) + version = False + if opinion: + path = os.path.join(path, "log") + if appname and version: + path = os.path.join(path, version) + return path + + +class AppDirs(object): + """Convenience wrapper for getting application dirs.""" + def __init__(self, appname=None, appauthor=None, version=None, + roaming=False, multipath=False): + self.appname = appname + self.appauthor = appauthor + self.version = version + self.roaming = roaming + self.multipath = multipath + + @property + def user_data_dir(self): + return user_data_dir(self.appname, self.appauthor, + version=self.version, roaming=self.roaming) + + @property + def site_data_dir(self): + return site_data_dir(self.appname, self.appauthor, + version=self.version, multipath=self.multipath) + + @property + def user_config_dir(self): + return user_config_dir(self.appname, self.appauthor, + version=self.version, roaming=self.roaming) + + @property + def site_config_dir(self): + return site_config_dir(self.appname, self.appauthor, + version=self.version, multipath=self.multipath) + + @property + def user_cache_dir(self): + return user_cache_dir(self.appname, self.appauthor, + version=self.version) + + @property + def user_state_dir(self): + return user_state_dir(self.appname, self.appauthor, + version=self.version) + + @property + def user_log_dir(self): + return user_log_dir(self.appname, self.appauthor, + version=self.version) + + +#---- internal support stuff + +def _get_win_folder_from_registry(csidl_name): + """This is a fallback technique at best. I'm not sure if using the + registry for this guarantees us the correct answer for all CSIDL_* + names. + """ + if PY3: + import winreg as _winreg + else: + import _winreg + + shell_folder_name = { + "CSIDL_APPDATA": "AppData", + "CSIDL_COMMON_APPDATA": "Common AppData", + "CSIDL_LOCAL_APPDATA": "Local AppData", + }[csidl_name] + + key = _winreg.OpenKey( + _winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" + ) + dir, type = _winreg.QueryValueEx(key, shell_folder_name) + return dir + + +def _get_win_folder_with_pywin32(csidl_name): + from win32com.shell import shellcon, shell + dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0) + # Try to make this a unicode path because SHGetFolderPath does + # not return unicode strings when there is unicode data in the + # path. + try: + dir = unicode(dir) + + # Downgrade to short path name if have highbit chars. See + # . + has_high_char = False + for c in dir: + if ord(c) > 255: + has_high_char = True + break + if has_high_char: + try: + import win32api + dir = win32api.GetShortPathName(dir) + except ImportError: + pass + except UnicodeError: + pass + return dir + + +def _get_win_folder_with_ctypes(csidl_name): + import ctypes + + csidl_const = { + "CSIDL_APPDATA": 26, + "CSIDL_COMMON_APPDATA": 35, + "CSIDL_LOCAL_APPDATA": 28, + }[csidl_name] + + buf = ctypes.create_unicode_buffer(1024) + ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) + + # Downgrade to short path name if have highbit chars. See + # . + has_high_char = False + for c in buf: + if ord(c) > 255: + has_high_char = True + break + if has_high_char: + buf2 = ctypes.create_unicode_buffer(1024) + if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): + buf = buf2 + + return buf.value + +def _get_win_folder_with_jna(csidl_name): + import array + from com.sun import jna + from com.sun.jna.platform import win32 + + buf_size = win32.WinDef.MAX_PATH * 2 + buf = array.zeros('c', buf_size) + shell = win32.Shell32.INSTANCE + shell.SHGetFolderPath(None, getattr(win32.ShlObj, csidl_name), None, win32.ShlObj.SHGFP_TYPE_CURRENT, buf) + dir = jna.Native.toString(buf.tostring()).rstrip("\0") + + # Downgrade to short path name if have highbit chars. See + # . + has_high_char = False + for c in dir: + if ord(c) > 255: + has_high_char = True + break + if has_high_char: + buf = array.zeros('c', buf_size) + kernel = win32.Kernel32.INSTANCE + if kernel.GetShortPathName(dir, buf, buf_size): + dir = jna.Native.toString(buf.tostring()).rstrip("\0") + + return dir + +if system == "win32": + try: + import win32com.shell + _get_win_folder = _get_win_folder_with_pywin32 + except ImportError: + try: + from ctypes import windll + _get_win_folder = _get_win_folder_with_ctypes + except ImportError: + try: + import com.sun.jna + _get_win_folder = _get_win_folder_with_jna + except ImportError: + _get_win_folder = _get_win_folder_from_registry + + +#---- self test code + +if __name__ == "__main__": + appname = "MyApp" + appauthor = "MyCompany" + + props = ("user_data_dir", + "user_config_dir", + "user_cache_dir", + "user_state_dir", + "user_log_dir", + "site_data_dir", + "site_config_dir") + + print("-- app dirs %s --" % __version__) + + print("-- app dirs (with optional 'version')") + dirs = AppDirs(appname, appauthor, version="1.0") + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) + + print("\n-- app dirs (without optional 'version')") + dirs = AppDirs(appname, appauthor) + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) + + print("\n-- app dirs (without optional 'appauthor')") + dirs = AppDirs(appname) + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) + + print("\n-- app dirs (with disabled 'appauthor')") + dirs = AppDirs(appname, appauthor=False) + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) From ef4bc0e6e53dbe52872699898196a459ded17aa2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 9 Nov 2018 17:35:54 +0100 Subject: [PATCH 09/16] action DJV sets start/end frame by files in folder and fps by ftrack --- pype/ftrack/actions/djvview.py | 40 +++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/pype/ftrack/actions/djvview.py b/pype/ftrack/actions/djvview.py index 31c1812662..410fa4fde5 100644 --- a/pype/ftrack/actions/djvview.py +++ b/pype/ftrack/actions/djvview.py @@ -208,19 +208,43 @@ class DJVViewAction(object): # Launching application if "values" in event["data"]: - filename = event['data']['values']['path'] + file_type = filename.split(".")[-1] + + # TODO Is this proper way? + try: + fps = int(entities[0]['custom_attributes']['fps']) + except: + fps = 24 - # TODO These should be obtained in another way - start = 375 - end = 379 - fps = 24 # TODO issequence is probably already built-in validation in ftrack isseq = re.findall('%[0-9]*d', filename) if len(isseq) > 0: - padding = re.findall('%[0-9]*d', filename).pop() - range = (padding % start) + '-' + (padding % end) - filename = re.sub('%[0-9]*d', range, filename) + if len(isseq) == 1: + frames = [] + padding = re.findall('%[0-9]*d', filename).pop() + index = filename.find(padding) + + full_file = filename[0:index-1] + file = full_file.split(os.sep)[-1] + folder = os.path.dirname(full_file) + + for fname in os.listdir(path=folder): + if fname.endswith(file_type) and file in fname: + frames.append(int(fname.split(".")[-2])) + + if len(frames) > 0: + start = min(frames) + end = max(frames) + + range = (padding % start) + '-' + (padding % end) + filename = re.sub('%[0-9]*d', range, filename) + else: + print("") + return { + 'success': False, + 'message': 'DJV View - Filename has more than one seqence identifier.' + } cmd = [] # DJV path From 50b015115c19ae9fcedd343f4369eaad565185df Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 12 Nov 2018 13:07:45 +0100 Subject: [PATCH 10/16] change imports for implementation into pype-start --- pype/ftrack/__init__.py | 15 --------------- pype/ftrack/credentials.py | 5 +++-- pype/ftrack/login_dialog.py | 15 ++++----------- pype/ftrack/login_tools.py | 8 +------- 4 files changed, 8 insertions(+), 35 deletions(-) diff --git a/pype/ftrack/__init__.py b/pype/ftrack/__init__.py index 44f9b76bcc..e69de29bb2 100644 --- a/pype/ftrack/__init__.py +++ b/pype/ftrack/__init__.py @@ -1,15 +0,0 @@ -import credentials -import login_dialog - -cred = credentials._get_credentials() - -if 'username' in cred and 'apiKey' in cred: - validation = credentials._check_credentials( - cred['username'], - cred['apiKey'] - ) - if validation is False: - login_dialog.run_login() - -else: - login_dialog.run_login() diff --git a/pype/ftrack/credentials.py b/pype/ftrack/credentials.py index 46292ebee7..74b0b298ab 100644 --- a/pype/ftrack/credentials.py +++ b/pype/ftrack/credentials.py @@ -1,10 +1,11 @@ import os import toml + import ftrack_api -import appdirs +import appdirs config_path = os.path.normpath(appdirs.user_data_dir('pype-app','pype')) -config_name = 'credentials.toml' +config_name = 'ftrack_cred.toml' fpath = os.path.join(config_path, config_name) def _get_credentials(): diff --git a/pype/ftrack/login_dialog.py b/pype/ftrack/login_dialog.py index 0e671c7ffd..2a69f92f69 100644 --- a/pype/ftrack/login_dialog.py +++ b/pype/ftrack/login_dialog.py @@ -1,17 +1,10 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file '..\CODE\github\pypeclub\pype-setup\temp\pype_project_settins_ui\login_dialogue.ui' -# -# Created by: PyQt5 UI code generator 5.7.1 -# -# WARNING! All changes made in this file will be lost! - import sys +import requests from PyQt5 import QtCore, QtGui, QtWidgets from app import style -import credentials -import login_tools -import requests +from . import credentials, login_tools + + class Login_Dialog_ui(QtWidgets.QWidget): SIZE_W = 300 diff --git a/pype/ftrack/login_tools.py b/pype/ftrack/login_tools.py index 200638b3df..3c0f430c9e 100644 --- a/pype/ftrack/login_tools.py +++ b/pype/ftrack/login_tools.py @@ -1,13 +1,7 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2016 ftrack - from http.server import BaseHTTPRequestHandler, HTTPServer -# import BaseHTTPServer from urllib import parse -# import urlparse import webbrowser import functools -# from QtExt import QtCore from PyQt5 import QtCore # class LoginServerHandler(BaseHTTPServer.BaseHTTPRequestHandler): @@ -68,7 +62,7 @@ class LoginServerHandler(BaseHTTPRequestHandler): self.end_headers() self.wfile.write(message.encode()) - + if login_credentials: self.login_callback( api_user, From 39a1c2768cf0d2fa028418598a90ae7edfb53cac Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 13 Nov 2018 11:05:15 +0100 Subject: [PATCH 11/16] Login without GUI --- pype/ftrack/Login/login_dialog_ui.py | 304 ++++++++++++ .../Login/templatesforcode/login_dialogue.py | 198 -------- .../templatesforcode/login_dialogue_usage.py | 196 -------- .../Login/templatesforcode/login_setup.py | 442 ------------------ pype/ftrack/login_dialog.py | 223 +-------- 5 files changed, 322 insertions(+), 1041 deletions(-) create mode 100644 pype/ftrack/Login/login_dialog_ui.py delete mode 100644 pype/ftrack/Login/templatesforcode/login_dialogue.py delete mode 100644 pype/ftrack/Login/templatesforcode/login_dialogue_usage.py delete mode 100644 pype/ftrack/Login/templatesforcode/login_setup.py diff --git a/pype/ftrack/Login/login_dialog_ui.py b/pype/ftrack/Login/login_dialog_ui.py new file mode 100644 index 0000000000..00d0c5ae5f --- /dev/null +++ b/pype/ftrack/Login/login_dialog_ui.py @@ -0,0 +1,304 @@ +import sys +import requests +import PyQt5 +from Qt.PyQt5 import QtCore, QtGui, QtWidgets +from app import style +from . import credentials, login_tools + + +class Login_Dialog_ui(QtWidgets.QWidget): + + SIZE_W = 300 + SIZE_H = 230 + + loginSignal = QtCore.pyqtSignal(object, object, object) + _login_server_thread = None + inputs = [] + buttons = [] + labels = [] + + def __init__(self): + + super().__init__() + self.loginSignal.connect(self.loginWithCredentials) + self._translate = QtCore.QCoreApplication.translate + + self.font = QtGui.QFont() + self.font.setFamily("DejaVu Sans Condensed") + self.font.setPointSize(9) + self.font.setBold(True) + self.font.setWeight(50) + self.font.setKerning(True) + + self.resize(self.SIZE_W, self.SIZE_H) + self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) + self.setStyleSheet(style.load_stylesheet()) + + self.setLayout(self._main()) + self.setWindowTitle('FTrack Login') + + self.show() + + def _main(self): + self.main = QtWidgets.QVBoxLayout() + self.main.setObjectName("main") + + self.form = QtWidgets.QFormLayout() + self.form.setContentsMargins(10, 15, 10, 5) + self.form.setObjectName("form") + + self.ftsite_label = QtWidgets.QLabel("FTrack URL:") + self.ftsite_label.setFont(self.font) + self.ftsite_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.ftsite_label.setTextFormat(QtCore.Qt.RichText) + self.ftsite_label.setObjectName("user_label") + + self.ftsite_input = QtWidgets.QLineEdit() + self.ftsite_input.setEnabled(True) + self.ftsite_input.setFrame(True) + self.ftsite_input.setEnabled(False) + self.ftsite_input.setReadOnly(True) + self.ftsite_input.setObjectName("ftsite_input") + + self.user_label = QtWidgets.QLabel("Username:") + self.user_label.setFont(self.font) + self.user_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.user_label.setTextFormat(QtCore.Qt.RichText) + self.user_label.setObjectName("user_label") + + self.user_input = QtWidgets.QLineEdit() + self.user_input.setEnabled(True) + self.user_input.setFrame(True) + self.user_input.setObjectName("user_input") + self.user_input.setPlaceholderText(self._translate("main","user.name")) + self.user_input.textChanged.connect(self._user_changed) + + self.api_label = QtWidgets.QLabel("API Key:") + self.api_label.setFont(self.font) + self.api_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.api_label.setTextFormat(QtCore.Qt.RichText) + self.api_label.setObjectName("api_label") + + self.api_input = QtWidgets.QLineEdit() + self.api_input.setEnabled(True) + self.api_input.setFrame(True) + self.api_input.setObjectName("api_input") + self.api_input.setPlaceholderText(self._translate("main","e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")) + self.api_input.textChanged.connect(self._api_changed) + + self.error_label = QtWidgets.QLabel("") + self.error_label.setFont(self.font) + self.error_label.setTextFormat(QtCore.Qt.RichText) + self.error_label.setObjectName("error_label") + self.error_label.setWordWrap(True); + self.error_label.hide() + + self.form.addRow(self.ftsite_label, self.ftsite_input) + self.form.addRow(self.user_label, self.user_input) + self.form.addRow(self.api_label,self.api_input) + self.form.addRow(self.error_label) + + self.btnGroup = QtWidgets.QHBoxLayout() + self.btnGroup.addStretch(1) + self.btnGroup.setObjectName("btnGroup") + + self.btnEnter = QtWidgets.QPushButton("Login") + self.btnEnter.setToolTip('Set Username and API Key with entered values') + self.btnEnter.clicked.connect(self.enter_credentials) + + self.btnClose = QtWidgets.QPushButton("Close") + self.btnClose.setToolTip('Close this window') + self.btnClose.clicked.connect(self._close_widget) + + self.btnFtrack = QtWidgets.QPushButton("Ftrack") + self.btnFtrack.setToolTip('Open browser for Login to Ftrack') + self.btnFtrack.clicked.connect(self.open_ftrack) + + self.btnGroup.addWidget(self.btnFtrack) + self.btnGroup.addWidget(self.btnEnter) + self.btnGroup.addWidget(self.btnClose) + + self.main.addLayout(self.form) + self.main.addLayout(self.btnGroup) + + self.inputs.append(self.api_input) + self.inputs.append(self.user_input) + self.inputs.append(self.ftsite_input) + + self.enter_site() + return self.main + + def enter_site(self): + try: + # # TESTING + # url = ".ftrackapp.com" + url = os.getenv('FTRACK_SERVER') + newurl = self.checkUrl(url) + + if newurl is None: + self.btnEnter.setEnabled(False) + self.btnFtrack.setEnabled(False) + for input in self.inputs: + input.setEnabled(False) + newurl = url + + self.ftsite_input.setText(newurl) + + except: + self.setError("FTRACK_SERVER is not set in templates") + self.btnEnter.setEnabled(False) + self.btnFtrack.setEnabled(False) + for input in self.inputs: + input.setEnabled(False) + + def setError(self, msg): + self.error_label.setText(msg) + self.error_label.show() + + def _user_changed(self): + self.user_input.setStyleSheet("") + + def _api_changed(self): + self.api_input.setStyleSheet("") + + def _invalid_input(self,entity): + entity.setStyleSheet("border: 1px solid red;") + + def enter_credentials(self): + user = self.user_input.text().strip() + api = self.api_input.text().strip() + msg = "You didn't enter " + missing = [] + if user == "": + missing.append("Username") + self._invalid_input(self.user_input) + + if api == "": + missing.append("API Key") + self._invalid_input(self.api_input) + + if len(missing) > 0: + self.setError("{0} {1}".format(msg, " and ".join(missing))) + return + + verification = credentials._check_credentials(user, api) + + if verification: + credentials._save_credentials(username, apiKey) + credentials._set_env(username, apiKey) + self._close_widget() + else: + self._invalid_input(self.user_input) + self._invalid_input(self.api_input) + self.setError("We're unable to connect to Ftrack with these credentials") + + def open_ftrack(self): + url = self.ftsite_input.text() + self.loginWithCredentials(url,None,None) + + def checkUrl(self, url): + url = url.strip('/ ') + + if not url: + self.setError() + 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: + self.setError( + 'The server URL set in Templates could not be reached.' + ) + return + + + if ( + result.status_code != 200 or 'FTRACK_VERSION' not in result.headers + ): + self.setError( + '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: + self.setError( + '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: + self.setError( + 'The server URL you provided could not be reached.' + ) + return + + + if ( + result.status_code != 200 or 'FTRACK_VERSION' not in result.headers + ): + self.setError( + '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) + return + + verification = credentials._check_credentials(user, api) + + if verification is True: + credentials._save_credentials(username, apiKey) + credentials._set_env(username, apiKey) + self._close_widget() + + + def _close_widget(self): + sys.exit(app.exec_()) + + +class Login_Dialog(Login_Dialog_ui): + def __init__(self): + super(Login_Dialog, self).__init__() + + +def getApp(): + return QtWidgets.QApplication(sys.argv) + +def run_login(): + app = getApp() + ui = Login_Dialog() + ui.show() + sys.exit(app.exec_()) diff --git a/pype/ftrack/Login/templatesforcode/login_dialogue.py b/pype/ftrack/Login/templatesforcode/login_dialogue.py deleted file mode 100644 index ba0315cabf..0000000000 --- a/pype/ftrack/Login/templatesforcode/login_dialogue.py +++ /dev/null @@ -1,198 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file '..\CODE\github\pypeclub\pype-setup\temp\pype_project_settins_ui\login_dialogue.ui' -# -# Created by: PyQt5 UI code generator 5.7.1 -# -# WARNING! All changes made in this file will be lost! - -import sys -from PyQt5 import QtCore, QtGui, QtWidgets - -from app import style - - - - -class Login_Dialog_ui(object): - - SIZE_W = 250 - SIZE_H = 300 - - def __init__(self): - super(Login_Dialog_ui, self).__init__() - self.Dialog = QtWidgets.QDialog() - self.Dialog.setStyleSheet(style.load_stylesheet()) - self.Dialog.setObjectName("Dialog") - self.Dialog.resize(self.SIZE_W, self.SIZE_H) - self.Dialog.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) - self.verticalLayoutWidget = QtWidgets.QWidget(self.Dialog) - self.verticalLayoutWidget.setGeometry(QtCore.QRect(0, 0, self.SIZE_W + 1, self.SIZE_H + 1)) - self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") - self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget) - self.verticalLayout.setContentsMargins(10, 5, 10, 5) - self.verticalLayout.setObjectName("verticalLayout") - - font = QtGui.QFont() - font.setFamily("DejaVu Sans Condensed") - font.setPointSize(9) - font.setBold(True) - font.setWeight(50) - font.setKerning(True) - - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - - if self.ftracksite: - self.site_label = QtWidgets.QLabel(self.verticalLayoutWidget) - sizePolicy.setHeightForWidth(self.site_label.sizePolicy().hasHeightForWidth()) - self.site_label.setSizePolicy(sizePolicy) - self.site_label.setMinimumSize(QtCore.QSize(150, 28)) - self.site_label.setFont(font) - self.site_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.site_label.setTextFormat(QtCore.Qt.RichText) - # self.site_label.setAlignment(QtCore.Qt.AlignCenter) - self.site_label.setObjectName("site_label") - self.verticalLayout.addWidget(self.site_label) - - self.site_input = QtWidgets.QLineEdit(self.verticalLayoutWidget) - self.site_input.setEnabled(True) - self.site_input.setFrame(True) - self.site_input.setFrame(True) - self.site_input.setReadOnly(True) - self.site_input.setObjectName("site_input") - self.verticalLayout.addWidget(self.site_input) - - self.user_label = QtWidgets.QLabel(self.verticalLayoutWidget) - sizePolicy.setHeightForWidth(self.user_label.sizePolicy().hasHeightForWidth()) - self.user_label.setSizePolicy(sizePolicy) - self.user_label.setMinimumSize(QtCore.QSize(150, 28)) - self.user_label.setFont(font) - self.user_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.user_label.setTextFormat(QtCore.Qt.RichText) - # self.user_label.setAlignment(QtCore.Qt.AlignCenter) - self.user_label.setObjectName("user_label") - self.verticalLayout.addWidget(self.user_label) - - self.user_input = QtWidgets.QLineEdit(self.verticalLayoutWidget) - self.user_input.setEnabled(True) - self.user_input.setFrame(True) - self.user_input.setObjectName("user_input") - self.verticalLayout.addWidget(self.user_input) - - self.api_label = QtWidgets.QLabel(self.verticalLayoutWidget) - sizePolicy.setHeightForWidth(self.api_label.sizePolicy().hasHeightForWidth()) - self.api_label.setSizePolicy(sizePolicy) - self.api_label.setMinimumSize(QtCore.QSize(150, 28)) - self.api_label.setFont(font) - self.api_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.api_label.setTextFormat(QtCore.Qt.RichText) - # self.api_label.setAlignment(QtCore.Qt.AlignCenter) - self.api_label.setObjectName("api_label") - self.verticalLayout.addWidget(self.api_label) - - self.api_input = QtWidgets.QLineEdit(self.verticalLayoutWidget) - self.api_input.setEnabled(True) - self.api_input.setFrame(True) - self.api_input.setObjectName("api_input") - self.verticalLayout.addWidget(self.api_input) - - spacerItem = QtWidgets.QSpacerItem( - 20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout.addItem(spacerItem) - - - # button.setToolTip('This is an example button') - # button.move(100,70) - self.btn_ftrack = QtWidgets.QPushButton("Login", self.verticalLayoutWidget) - self.btn_ftrack.resize(10,10) - # self.btn_ftrack.move(100,70) - self.verticalLayout.addWidget(self.btn_ftrack) - - self.buttonBox = QtWidgets.QDialogButtonBox(self.verticalLayoutWidget) - self.buttonBox.setOrientation(QtCore.Qt.Horizontal) - self.buttonBox.setStandardButtons( - QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok) - self.buttonBox.setObjectName("buttonBox") - self.verticalLayout.addWidget(self.buttonBox) - - self.retranslateUi(self.Dialog) - self.buttonBox.accepted.connect(self.execute) - self.buttonBox.rejected.connect(self.Dialog.reject) - QtCore.QMetaObject.connectSlotsByName(self.Dialog) - self.Dialog.setTabOrder(self.user_input, self.api_input) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", self.ui_title)) - self.site_label.setText(_translate("Dialog", "FTrack URL:")) - if self.ftracksite: - self.site_input.setText(_translate("Dialog", self.ftracksite)) - self.user_label.setText(_translate("Dialog", "Username:")) - self.user_input.setPlaceholderText(_translate("Dialog", "user.name")) - self.api_label.setText(_translate("Dialog", "API Key:")) - self.api_input.setPlaceholderText(_translate("Dialog", "eg.:")) - - def show(self): - self.Dialog.show() - - -class Login_Dialog(Login_Dialog_ui): - def __init__(self, ui_title="Dialog", ftracksite=None): - self.ui_title = ui_title - self.ftracksite = ftracksite - super(Login_Dialog, self).__init__() - self.user_input.textChanged.connect(self._user_changed) - self.api_input.textChanged.connect(self._api_changed) - - def _user_changed(self): - self.user_input.setStyleSheet("") - - def _api_changed(self): - self.api_input.setStyleSheet("") - # print(self.passw_input.text()) - - def _invalid_input(self,entity): - entity.setStyleSheet("border: 1px solid red;") - - def _check_credentials(self): - logged = False - - user = self.user_input.text() - api = self.api_input.text() - - if user == "": - self._invalid_input(self.user_input) - elif True: - # IF user exist - pass - - if api == "": - self._invalid_input(self.api_input) - elif True: - # IF is api ok exist - Session creation - pass - - if logged is True: - self.close() - - def execute(self): - self._check_credentials() - - -def getApp(): - return QtWidgets.QApplication(sys.argv) - -def main(): - app = QtWidgets.QApplication(sys.argv) - ui = Login_Dialog("Ftrack Login","pype") - ui.show() - sys.exit(app.exec_()) - - -if __name__ == "__main__": - main() - -main() diff --git a/pype/ftrack/Login/templatesforcode/login_dialogue_usage.py b/pype/ftrack/Login/templatesforcode/login_dialogue_usage.py deleted file mode 100644 index ac6e986bef..0000000000 --- a/pype/ftrack/Login/templatesforcode/login_dialogue_usage.py +++ /dev/null @@ -1,196 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file '..\CODE\github\pypeclub\pype-setup\temp\pype_project_settins_ui\login_dialogue.ui' -# -# Created by: PyQt5 UI code generator 5.7.1 -# -# WARNING! All changes made in this file will be lost! - -import sys -from PyQt5 import QtCore, QtGui, QtWidgets - -from app import style - - -class Login_Dialog_ui(object): - - SIZE_W = 250 - SIZE_H = 300 - - def __init__(self): - super(Login_Dialog_ui, self).__init__() - self.Dialog = QtWidgets.QDialog() - self.Dialog.setStyleSheet(style.load_stylesheet()) - self.Dialog.setObjectName("Dialog") - self.Dialog.resize(self.SIZE_W, self.SIZE_H) - self.Dialog.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) - self.verticalLayoutWidget = QtWidgets.QWidget(self.Dialog) - self.verticalLayoutWidget.setGeometry(QtCore.QRect(0, 0, self.SIZE_W + 1, self.SIZE_H + 1)) - self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") - self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget) - self.verticalLayout.setContentsMargins(10, 5, 10, 5) - self.verticalLayout.setObjectName("verticalLayout") - - font = QtGui.QFont() - font.setFamily("DejaVu Sans Condensed") - font.setPointSize(9) - font.setBold(True) - font.setWeight(50) - font.setKerning(True) - - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - - if self.ftracksite: - self.site_label = QtWidgets.QLabel(self.verticalLayoutWidget) - sizePolicy.setHeightForWidth(self.site_label.sizePolicy().hasHeightForWidth()) - self.site_label.setSizePolicy(sizePolicy) - self.site_label.setMinimumSize(QtCore.QSize(150, 28)) - self.site_label.setFont(font) - self.site_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.site_label.setTextFormat(QtCore.Qt.RichText) - # self.site_label.setAlignment(QtCore.Qt.AlignCenter) - self.site_label.setObjectName("site_label") - self.verticalLayout.addWidget(self.site_label) - - self.site_input = QtWidgets.QLineEdit(self.verticalLayoutWidget) - self.site_input.setEnabled(True) - self.site_input.setFrame(True) - self.site_input.setFrame(True) - self.site_input.setReadOnly(True) - self.site_input.setObjectName("site_input") - self.verticalLayout.addWidget(self.site_input) - - self.user_label = QtWidgets.QLabel(self.verticalLayoutWidget) - sizePolicy.setHeightForWidth(self.user_label.sizePolicy().hasHeightForWidth()) - self.user_label.setSizePolicy(sizePolicy) - self.user_label.setMinimumSize(QtCore.QSize(150, 28)) - self.user_label.setFont(font) - self.user_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.user_label.setTextFormat(QtCore.Qt.RichText) - # self.user_label.setAlignment(QtCore.Qt.AlignCenter) - self.user_label.setObjectName("user_label") - self.verticalLayout.addWidget(self.user_label) - - self.user_input = QtWidgets.QLineEdit(self.verticalLayoutWidget) - self.user_input.setEnabled(True) - self.user_input.setFrame(True) - self.user_input.setObjectName("user_input") - self.verticalLayout.addWidget(self.user_input) - - self.api_label = QtWidgets.QLabel(self.verticalLayoutWidget) - sizePolicy.setHeightForWidth(self.api_label.sizePolicy().hasHeightForWidth()) - self.api_label.setSizePolicy(sizePolicy) - self.api_label.setMinimumSize(QtCore.QSize(150, 28)) - self.api_label.setFont(font) - self.api_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.api_label.setTextFormat(QtCore.Qt.RichText) - # self.api_label.setAlignment(QtCore.Qt.AlignCenter) - self.api_label.setObjectName("api_label") - self.verticalLayout.addWidget(self.api_label) - - self.api_input = QtWidgets.QLineEdit(self.verticalLayoutWidget) - self.api_input.setEnabled(True) - self.api_input.setFrame(True) - self.api_input.setObjectName("api_input") - self.verticalLayout.addWidget(self.api_input) - - spacerItem = QtWidgets.QSpacerItem( - 20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout.addItem(spacerItem) - - - # button.setToolTip('This is an example button') - # button.move(100,70) - self.btn_ftrack = QtWidgets.QPushButton("Login", self.verticalLayoutWidget) - self.btn_ftrack.resize(10,10) - # self.btn_ftrack.move(100,70) - self.verticalLayout.addWidget(self.btn_ftrack) - - self.buttonBox = QtWidgets.QDialogButtonBox(self.verticalLayoutWidget) - self.buttonBox.setOrientation(QtCore.Qt.Horizontal) - self.buttonBox.setStandardButtons( - QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok) - self.buttonBox.setObjectName("buttonBox") - self.verticalLayout.addWidget(self.buttonBox) - - self.retranslateUi(self.Dialog) - self.buttonBox.accepted.connect(self.execute) - self.buttonBox.rejected.connect(self.Dialog.reject) - QtCore.QMetaObject.connectSlotsByName(self.Dialog) - self.Dialog.setTabOrder(self.user_input, self.api_input) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", self.ui_title)) - self.site_label.setText(_translate("Dialog", "FTrack URL:")) - if self.ftracksite: - self.site_input.setText(_translate("Dialog", self.ftracksite)) - self.user_label.setText(_translate("Dialog", "Username:")) - self.user_input.setPlaceholderText(_translate("Dialog", "user.name")) - self.api_label.setText(_translate("Dialog", "API Key:")) - self.api_input.setPlaceholderText(_translate("Dialog", "eg.:")) - - def show(self): - self.Dialog.show() - - -class Login_Dialog(Login_Dialog_ui): - def __init__(self, ui_title="Dialog", ftracksite=None): - self.ui_title = ui_title - self.ftracksite = ftracksite - super(Login_Dialog, self).__init__() - self.user_input.textChanged.connect(self._user_changed) - self.api_input.textChanged.connect(self._api_changed) - - def _user_changed(self): - self.user_input.setStyleSheet("") - - def _api_changed(self): - self.api_input.setStyleSheet("") - # print(self.passw_input.text()) - - def _invalid_input(self,entity): - entity.setStyleSheet("border: 1px solid red;") - - def _check_credentials(self): - logged = False - - user = self.user_input.text() - api = self.api_input.text() - - if user == "": - self._invalid_input(self.user_input) - elif True: - # IF user exist - pass - - if api == "": - self._invalid_input(self.api_input) - elif True: - # IF is api ok exist - Session creation - pass - - if logged is True: - self.close() - - def execute(self): - self._check_credentials() - - -def getApp(): - return QtWidgets.QApplication(sys.argv) - -def main(): - app = getApp() - ui = Login_Dialog("Ftrack Login","pype") - ui.show() - sys.exit(app.exec_()) - - -if __name__ == "__main__": - main() - -main() diff --git a/pype/ftrack/Login/templatesforcode/login_setup.py b/pype/ftrack/Login/templatesforcode/login_setup.py deleted file mode 100644 index 6ebae672fa..0000000000 --- a/pype/ftrack/Login/templatesforcode/login_setup.py +++ /dev/null @@ -1,442 +0,0 @@ -import os -import sys -import login_tools -from PyQt5 import QtCore -import requests - - -class FtrackLogin(object): - - # loginSignal = QtCore.pyqtSignal(object, object, object) - # - # def __init__(self): - # self.username = None - # self.apiKey = None - # self.url = "https://pype.ftrackapp.com" - # - # self._login_server_thread = None - # self.loginSignal.connect(self.loginWithCredentials) - # self.login() - # - # def login(self): - # '''Login using stored credentials or ask user for them.''' - # - # credentials = self._get_credentials() - # if credentials: - # # Try to login. - # self.loginWithCredentials( - # credentials['server_url'], - # credentials['api_user'], - # credentials['api_key'] - # ) - # - # def setup_session(self): - # try: - # session = ftrack_api.session() - # except Exception as e: - # return False - # return session - # - # def report_session_setup_error(self, error): - # msg = ( - # u'\nAn error occured while starting ftrack: {0}.'.format(error) - # ) - # print(msg) - # # self.loginError.emit(msg) - # - # def _get_credentials(self): - # data = {'server_url':self.url, - # 'api_user':self.username, - # 'api_key':self.apiKey - # } - # return data - # - # def _save_credentials(self, url, username, apiKey): - # self.url = url - # self.username = username - # self.apiKey = apiKey - # - # def loginWithCredentials(self, url, username, apiKey): - # url = url.strip('/ ') - # - # if not url: - # self.loginError.emit( - # '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: - # self.logger.exception('Error reaching server url.') - # self.loginError.emit( - # 'The server URL you provided could not be reached.' - # ) - # return - # - # if ( - # result.status_code != 200 or 'FTRACK_VERSION' not in result.headers - # ): - # self.loginError.emit( - # '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) - # return - # - # # Set environment variables supported by the old API. - # os.environ['FTRACK_SERVER'] = url - # os.environ['LOGNAME'] = username - # os.environ['FTRACK_APIKEY'] = apiKey - # - # # Set environment variables supported by the new API. - # os.environ['FTRACK_API_USER'] = username - # os.environ['FTRACK_API_KEY'] = apiKey - # - # # Login using the new ftrack API. - # try: - # self._session = self._setup_session() - # except Exception as error: - # self.logger.exception(u'Error during login.:') - # self._report_session_setup_error(error) - # return - # - # # Store credentials since login was successful. - # self._save_credentials(url, username, apiKey) - # - # # Verify storage scenario before starting. - # if 'storage_scenario' in self._session.server_information: - # storage_scenario = self._session.server_information.get( - # 'storage_scenario' - # ) - # if storage_scenario is None: - # # Hide login overlay at this time since it will be deleted - # self.logger.debug('Storage scenario is not configured.') - # scenario_widget = _scenario_widget.ConfigureScenario( - # self._session - # ) - # scenario_widget.configuration_completed.connect( - # self.location_configuration_finished - # ) - # self.setCentralWidget(scenario_widget) - # self.focus() - # return - - - - - - - - - - - - - - - - - - - - - - - - - - - loginError = QtCore.pyqtSignal(object) - - #: Signal when event received via ftrack's event hub. - eventHubSignal = QtCore.pyqtSignal(object) - - # Login signal. - loginSignal = QtCore.pyqtSignal(object, object, object) - - def __init__(self, *args, **kwargs): - - # self.logger = logging.getLogger( - # __name__ + '.' + self.__class__.__name__ - # ) - - self._login_server_thread = None - - self._login_overlay = None - self.loginSignal.connect(self.loginWithCredentials) - self.login() - - - def _onConnectTopicEvent(self, event): - '''Generic callback for all ftrack.connect events. - - .. note:: - Events not triggered by the current logged in user will be dropped. - - ''' - if event['topic'] != 'ftrack.connect': - return - - self._routeEvent(event) - - def logout(self): - '''Clear stored credentials and quit Connect.''' - self._clear_qsettings() - config = ftrack_connect.ui.config.read_json_config() - - config['accounts'] = [] - ftrack_connect.ui.config.write_json_config(config) - - QtWidgets.qApp.quit() - - def _clear_qsettings(self): - '''Remove credentials from QSettings.''' - settings = QtCore.QSettings() - settings.remove('login') - - def _get_credentials(self): - '''Return a dict with API credentials from storage.''' - credentials = None - - # Read from json config file. - json_config = ftrack_connect.ui.config.read_json_config() - if json_config: - try: - data = json_config['accounts'][0] - credentials = { - 'server_url': data['server_url'], - 'api_user': data['api_user'], - 'api_key': data['api_key'] - } - except Exception: - self.logger.debug( - u'No credentials were found in config: {0}.'.format( - json_config - ) - ) - - # Fallback on old QSettings. - if not json_config and not credentials: - settings = QtCore.QSettings() - server_url = settings.value('login/server', None) - api_user = settings.value('login/username', None) - api_key = settings.value('login/apikey', None) - - if not None in (server_url, api_user, api_key): - credentials = { - 'server_url': server_url, - 'api_user': api_user, - 'api_key': api_key - } - - return credentials - - def _save_credentials(self, server_url, api_user, api_key): - '''Save API credentials to storage.''' - # Clear QSettings since they should not be used any more. - self._clear_qsettings() - - # Save the credentials. - json_config = ftrack_connect.ui.config.read_json_config() - - if not json_config: - json_config = {} - - # Add a unique id to the config that can be used to identify this - # machine. - if not 'id' in json_config: - json_config['id'] = str(uuid.uuid4()) - - json_config['accounts'] = [{ - 'server_url': server_url, - 'api_user': api_user, - 'api_key': api_key - }] - - ftrack_connect.ui.config.write_json_config(json_config) - - def login(self): - '''Login using stored credentials or ask user for them.''' - credentials = self._get_credentials() - self.showLoginWidget() - - if credentials: - # Try to login. - self.loginWithCredentials( - credentials['server_url'], - credentials['api_user'], - credentials['api_key'] - ) - - def showLoginWidget(self): - '''Show the login widget.''' - self._login_overlay = ftrack_connect.ui.widget.overlay.CancelOverlay( - self.loginWidget, - message='Signing in' - ) - - self._login_overlay.hide() - self.setCentralWidget(self.loginWidget) - self.loginWidget.login.connect(self._login_overlay.show) - self.loginWidget.login.connect(self.loginWithCredentials) - self.loginError.connect(self.loginWidget.loginError.emit) - self.loginError.connect(self._login_overlay.hide) - self.focus() - - # Set focus on the login widget to remove any focus from its child - # widgets. - self.loginWidget.setFocus() - self._login_overlay.hide() - - def _setup_session(self): - '''Setup a new python API session.''' - if hasattr(self, '_hub_thread'): - self._hub_thread.quit() - - plugin_paths = os.environ.get( - 'FTRACK_EVENT_PLUGIN_PATH', '' - ).split(os.pathsep) - - plugin_paths.extend(self.pluginHookPaths) - - try: - session = ftrack_connect.session.get_shared_session( - plugin_paths=plugin_paths - ) - except Exception as error: - raise ftrack_connect.error.ParseError(error) - - # Listen to events using the new API event hub. This is required to - # allow reconfiguring the storage scenario. - self._hub_thread = _event_hub_thread.NewApiEventHubThread() - self._hub_thread.start(session) - - ftrack_api._centralized_storage_scenario.register_configuration( - session - ) - - return session - - def _report_session_setup_error(self, error): - '''Format error message and emit loginError.''' - msg = ( - u'\nAn error occured while starting ftrack-connect: {0}.' - u'\nPlease check log file for more informations.' - u'\nIf the error persists please send the log file to:' - u' support@ftrack.com'.format(error) - - ) - self.loginError.emit(msg) - - def loginWithCredentials(self, url, username, apiKey): - '''Connect to *url* with *username* and *apiKey*. - - loginError will be emitted if this fails. - - ''' - # Strip all leading and preceeding occurances of slash and space. - url = url.strip('/ ') - - if not url: - self.loginError.emit( - '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: - self.logger.exception('Error reaching server url.') - self.loginError.emit( - 'The server URL you provided could not be reached.' - ) - return - - if ( - result.status_code != 200 or 'FTRACK_VERSION' not in result.headers - ): - self.loginError.emit( - '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) - return - - # Set environment variables supported by the old API. - os.environ['FTRACK_SERVER'] = url - os.environ['LOGNAME'] = username - os.environ['FTRACK_APIKEY'] = apiKey - - # Set environment variables supported by the new API. - os.environ['FTRACK_API_USER'] = username - os.environ['FTRACK_API_KEY'] = apiKey - - # Login using the new ftrack API. - try: - self._session = self._setup_session() - except Exception as error: - self.logger.exception(u'Error during login.:') - self._report_session_setup_error(error) - return - - # Store credentials since login was successful. - self._save_credentials(url, username, apiKey) - - # Verify storage scenario before starting. - if 'storage_scenario' in self._session.server_information: - storage_scenario = self._session.server_information.get( - 'storage_scenario' - ) - if storage_scenario is None: - # Hide login overlay at this time since it will be deleted - self.logger.debug('Storage scenario is not configured.') - scenario_widget = _scenario_widget.ConfigureScenario( - self._session - ) - - self.setCentralWidget(scenario_widget) - self.focus() - return diff --git a/pype/ftrack/login_dialog.py b/pype/ftrack/login_dialog.py index 2a69f92f69..f6e3b33478 100644 --- a/pype/ftrack/login_dialog.py +++ b/pype/ftrack/login_dialog.py @@ -1,205 +1,38 @@ import sys import requests -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5 import QtCore, QtWidgets from app import style from . import credentials, login_tools -class Login_Dialog_ui(QtWidgets.QWidget): - - SIZE_W = 300 - SIZE_H = 230 +class Login_Dialog(QtCore.QObject): loginSignal = QtCore.pyqtSignal(object, object, object) _login_server_thread = None - inputs = [] - buttons = [] - labels = [] def __init__(self): - super().__init__() self.loginSignal.connect(self.loginWithCredentials) self._translate = QtCore.QCoreApplication.translate - self.font = QtGui.QFont() - self.font.setFamily("DejaVu Sans Condensed") - self.font.setPointSize(9) - self.font.setBold(True) - self.font.setWeight(50) - self.font.setKerning(True) - - self.resize(self.SIZE_W, self.SIZE_H) - self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) - self.setStyleSheet(style.load_stylesheet()) - - self.setLayout(self._main()) - self.setWindowTitle('FTrack Login') - - self.show() - - def _main(self): - self.main = QtWidgets.QVBoxLayout() - self.main.setObjectName("main") - - self.form = QtWidgets.QFormLayout() - self.form.setContentsMargins(10, 15, 10, 5) - self.form.setObjectName("form") - - self.ftsite_label = QtWidgets.QLabel("FTrack URL:") - self.ftsite_label.setFont(self.font) - self.ftsite_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.ftsite_label.setTextFormat(QtCore.Qt.RichText) - self.ftsite_label.setObjectName("user_label") - - self.ftsite_input = QtWidgets.QLineEdit() - self.ftsite_input.setEnabled(True) - self.ftsite_input.setFrame(True) - self.ftsite_input.setEnabled(False) - self.ftsite_input.setReadOnly(True) - self.ftsite_input.setObjectName("ftsite_input") - - self.user_label = QtWidgets.QLabel("Username:") - self.user_label.setFont(self.font) - self.user_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.user_label.setTextFormat(QtCore.Qt.RichText) - self.user_label.setObjectName("user_label") - - self.user_input = QtWidgets.QLineEdit() - self.user_input.setEnabled(True) - self.user_input.setFrame(True) - self.user_input.setObjectName("user_input") - self.user_input.setPlaceholderText(self._translate("main","user.name")) - self.user_input.textChanged.connect(self._user_changed) - - self.api_label = QtWidgets.QLabel("API Key:") - self.api_label.setFont(self.font) - self.api_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.api_label.setTextFormat(QtCore.Qt.RichText) - self.api_label.setObjectName("api_label") - - self.api_input = QtWidgets.QLineEdit() - self.api_input.setEnabled(True) - self.api_input.setFrame(True) - self.api_input.setObjectName("api_input") - self.api_input.setPlaceholderText(self._translate("main","e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")) - self.api_input.textChanged.connect(self._api_changed) - - self.error_label = QtWidgets.QLabel("") - self.error_label.setFont(self.font) - self.error_label.setTextFormat(QtCore.Qt.RichText) - self.error_label.setObjectName("error_label") - self.error_label.setWordWrap(True); - self.error_label.hide() - - self.form.addRow(self.ftsite_label, self.ftsite_input) - self.form.addRow(self.user_label, self.user_input) - self.form.addRow(self.api_label,self.api_input) - self.form.addRow(self.error_label) - - self.btnGroup = QtWidgets.QHBoxLayout() - self.btnGroup.addStretch(1) - self.btnGroup.setObjectName("btnGroup") - - self.btnEnter = QtWidgets.QPushButton("Login") - self.btnEnter.setToolTip('Set Username and API Key with entered values') - self.btnEnter.clicked.connect(self.enter_credentials) - - self.btnClose = QtWidgets.QPushButton("Close") - self.btnClose.setToolTip('Close this window') - self.btnClose.clicked.connect(self._close_widget) - - self.btnFtrack = QtWidgets.QPushButton("Ftrack") - self.btnFtrack.setToolTip('Open browser for Login to Ftrack') - self.btnFtrack.clicked.connect(self.open_ftrack) - - self.btnGroup.addWidget(self.btnFtrack) - self.btnGroup.addWidget(self.btnEnter) - self.btnGroup.addWidget(self.btnClose) - - self.main.addLayout(self.form) - self.main.addLayout(self.btnGroup) - - self.inputs.append(self.api_input) - self.inputs.append(self.user_input) - self.inputs.append(self.ftsite_input) - - self.enter_site() - return self.main - - def enter_site(self): + def run(self): try: - # # TESTING - # url = ".ftrackapp.com" url = os.getenv('FTRACK_SERVER') - newurl = self.checkUrl(url) - - if newurl is None: - self.btnEnter.setEnabled(False) - self.btnFtrack.setEnabled(False) - for input in self.inputs: - input.setEnabled(False) - newurl = url - - self.ftsite_input.setText(newurl) - - except: - self.setError("FTRACK_SERVER is not set in templates") - self.btnEnter.setEnabled(False) - self.btnFtrack.setEnabled(False) - for input in self.inputs: - input.setEnabled(False) - - def setError(self, msg): - self.error_label.setText(msg) - self.error_label.show() - - def _user_changed(self): - self.user_input.setStyleSheet("") - - def _api_changed(self): - self.api_input.setStyleSheet("") - - def _invalid_input(self,entity): - entity.setStyleSheet("border: 1px solid red;") - - def enter_credentials(self): - user = self.user_input.text().strip() - api = self.api_input.text().strip() - msg = "You didn't enter " - missing = [] - if user == "": - missing.append("Username") - self._invalid_input(self.user_input) - - if api == "": - missing.append("API Key") - self._invalid_input(self.api_input) - - if len(missing) > 0: - self.setError("{0} {1}".format(msg, " and ".join(missing))) return + except: + print("Environment variable 'FTRACK_SERVER' is not set.") - verification = credentials._check_credentials(user, api) - - if verification: - credentials._save_credentials(username, apiKey) - credentials._set_env(username, apiKey) - self._close_widget() - else: - self._invalid_input(self.user_input) - self._invalid_input(self.api_input) - self.setError("We're unable to connect to Ftrack with these credentials") + self.url = checkUrl(url) + self.open_ftrack() def open_ftrack(self): - url = self.ftsite_input.text() - self.loginWithCredentials(url,None,None) + self.loginWithCredentials(self.url,None,None) def checkUrl(self, url): url = url.strip('/ ') if not url: - self.setError() + print("Url is empty!") return if not 'http' in url: @@ -213,26 +46,23 @@ class Login_Dialog_ui(QtWidgets.QWidget): allow_redirects=False # Old python API will not work with redirect. ) except requests.exceptions.RequestException: - self.setError( - 'The server URL set in Templates could not be reached.' - ) + print('The server URL set in Templates could not be reached.') return if ( result.status_code != 200 or 'FTRACK_VERSION' not in result.headers ): - self.setError( - 'The server URL set in Templates is not a valid ftrack server.' - ) + 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: - self.setError( + print( 'You need to specify a valid server URL, ' 'for example https://server-name.ftrackapp.com' ) @@ -249,18 +79,14 @@ class Login_Dialog_ui(QtWidgets.QWidget): allow_redirects=False # Old python API will not work with redirect. ) except requests.exceptions.RequestException: - self.setError( - 'The server URL you provided could not be reached.' - ) + print('The server URL you provided could not be reached.') return if ( result.status_code != 200 or 'FTRACK_VERSION' not in result.headers ): - self.setError( - 'The server URL you provided is not a valid ftrack server.' - ) + 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. @@ -281,23 +107,10 @@ class Login_Dialog_ui(QtWidgets.QWidget): if verification is True: credentials._save_credentials(username, apiKey) credentials._set_env(username, apiKey) - self._close_widget() - def _close_widget(self): - sys.exit(app.exec_()) - - -class Login_Dialog(Login_Dialog_ui): - def __init__(self): - super(Login_Dialog, self).__init__() - - -def getApp(): - return QtWidgets.QApplication(sys.argv) - def run_login(): - app = getApp() - ui = Login_Dialog() - ui.show() + app = QtWidgets.QApplication(sys.argv) + applogin = Login_Dialog() + applogin.run() sys.exit(app.exec_()) From f345a40bff06b2aedd5b0ba74d0690267143fbd2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 13 Nov 2018 16:36:58 +0100 Subject: [PATCH 12/16] Able to log in and run server --- pype/ftrack/Login/login_dialog_ui.py | 304 --------------------------- pype/ftrack/__init__.py | 1 + pype/ftrack/credentials.py | 6 +- pype/ftrack/login_dialog.py | 229 ++++++++++++++++++-- pype/ftrack/login_dialog_noui.py | 120 +++++++++++ 5 files changed, 334 insertions(+), 326 deletions(-) delete mode 100644 pype/ftrack/Login/login_dialog_ui.py create mode 100644 pype/ftrack/login_dialog_noui.py diff --git a/pype/ftrack/Login/login_dialog_ui.py b/pype/ftrack/Login/login_dialog_ui.py deleted file mode 100644 index 00d0c5ae5f..0000000000 --- a/pype/ftrack/Login/login_dialog_ui.py +++ /dev/null @@ -1,304 +0,0 @@ -import sys -import requests -import PyQt5 -from Qt.PyQt5 import QtCore, QtGui, QtWidgets -from app import style -from . import credentials, login_tools - - -class Login_Dialog_ui(QtWidgets.QWidget): - - SIZE_W = 300 - SIZE_H = 230 - - loginSignal = QtCore.pyqtSignal(object, object, object) - _login_server_thread = None - inputs = [] - buttons = [] - labels = [] - - def __init__(self): - - super().__init__() - self.loginSignal.connect(self.loginWithCredentials) - self._translate = QtCore.QCoreApplication.translate - - self.font = QtGui.QFont() - self.font.setFamily("DejaVu Sans Condensed") - self.font.setPointSize(9) - self.font.setBold(True) - self.font.setWeight(50) - self.font.setKerning(True) - - self.resize(self.SIZE_W, self.SIZE_H) - self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) - self.setStyleSheet(style.load_stylesheet()) - - self.setLayout(self._main()) - self.setWindowTitle('FTrack Login') - - self.show() - - def _main(self): - self.main = QtWidgets.QVBoxLayout() - self.main.setObjectName("main") - - self.form = QtWidgets.QFormLayout() - self.form.setContentsMargins(10, 15, 10, 5) - self.form.setObjectName("form") - - self.ftsite_label = QtWidgets.QLabel("FTrack URL:") - self.ftsite_label.setFont(self.font) - self.ftsite_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.ftsite_label.setTextFormat(QtCore.Qt.RichText) - self.ftsite_label.setObjectName("user_label") - - self.ftsite_input = QtWidgets.QLineEdit() - self.ftsite_input.setEnabled(True) - self.ftsite_input.setFrame(True) - self.ftsite_input.setEnabled(False) - self.ftsite_input.setReadOnly(True) - self.ftsite_input.setObjectName("ftsite_input") - - self.user_label = QtWidgets.QLabel("Username:") - self.user_label.setFont(self.font) - self.user_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.user_label.setTextFormat(QtCore.Qt.RichText) - self.user_label.setObjectName("user_label") - - self.user_input = QtWidgets.QLineEdit() - self.user_input.setEnabled(True) - self.user_input.setFrame(True) - self.user_input.setObjectName("user_input") - self.user_input.setPlaceholderText(self._translate("main","user.name")) - self.user_input.textChanged.connect(self._user_changed) - - self.api_label = QtWidgets.QLabel("API Key:") - self.api_label.setFont(self.font) - self.api_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.api_label.setTextFormat(QtCore.Qt.RichText) - self.api_label.setObjectName("api_label") - - self.api_input = QtWidgets.QLineEdit() - self.api_input.setEnabled(True) - self.api_input.setFrame(True) - self.api_input.setObjectName("api_input") - self.api_input.setPlaceholderText(self._translate("main","e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")) - self.api_input.textChanged.connect(self._api_changed) - - self.error_label = QtWidgets.QLabel("") - self.error_label.setFont(self.font) - self.error_label.setTextFormat(QtCore.Qt.RichText) - self.error_label.setObjectName("error_label") - self.error_label.setWordWrap(True); - self.error_label.hide() - - self.form.addRow(self.ftsite_label, self.ftsite_input) - self.form.addRow(self.user_label, self.user_input) - self.form.addRow(self.api_label,self.api_input) - self.form.addRow(self.error_label) - - self.btnGroup = QtWidgets.QHBoxLayout() - self.btnGroup.addStretch(1) - self.btnGroup.setObjectName("btnGroup") - - self.btnEnter = QtWidgets.QPushButton("Login") - self.btnEnter.setToolTip('Set Username and API Key with entered values') - self.btnEnter.clicked.connect(self.enter_credentials) - - self.btnClose = QtWidgets.QPushButton("Close") - self.btnClose.setToolTip('Close this window') - self.btnClose.clicked.connect(self._close_widget) - - self.btnFtrack = QtWidgets.QPushButton("Ftrack") - self.btnFtrack.setToolTip('Open browser for Login to Ftrack') - self.btnFtrack.clicked.connect(self.open_ftrack) - - self.btnGroup.addWidget(self.btnFtrack) - self.btnGroup.addWidget(self.btnEnter) - self.btnGroup.addWidget(self.btnClose) - - self.main.addLayout(self.form) - self.main.addLayout(self.btnGroup) - - self.inputs.append(self.api_input) - self.inputs.append(self.user_input) - self.inputs.append(self.ftsite_input) - - self.enter_site() - return self.main - - def enter_site(self): - try: - # # TESTING - # url = ".ftrackapp.com" - url = os.getenv('FTRACK_SERVER') - newurl = self.checkUrl(url) - - if newurl is None: - self.btnEnter.setEnabled(False) - self.btnFtrack.setEnabled(False) - for input in self.inputs: - input.setEnabled(False) - newurl = url - - self.ftsite_input.setText(newurl) - - except: - self.setError("FTRACK_SERVER is not set in templates") - self.btnEnter.setEnabled(False) - self.btnFtrack.setEnabled(False) - for input in self.inputs: - input.setEnabled(False) - - def setError(self, msg): - self.error_label.setText(msg) - self.error_label.show() - - def _user_changed(self): - self.user_input.setStyleSheet("") - - def _api_changed(self): - self.api_input.setStyleSheet("") - - def _invalid_input(self,entity): - entity.setStyleSheet("border: 1px solid red;") - - def enter_credentials(self): - user = self.user_input.text().strip() - api = self.api_input.text().strip() - msg = "You didn't enter " - missing = [] - if user == "": - missing.append("Username") - self._invalid_input(self.user_input) - - if api == "": - missing.append("API Key") - self._invalid_input(self.api_input) - - if len(missing) > 0: - self.setError("{0} {1}".format(msg, " and ".join(missing))) - return - - verification = credentials._check_credentials(user, api) - - if verification: - credentials._save_credentials(username, apiKey) - credentials._set_env(username, apiKey) - self._close_widget() - else: - self._invalid_input(self.user_input) - self._invalid_input(self.api_input) - self.setError("We're unable to connect to Ftrack with these credentials") - - def open_ftrack(self): - url = self.ftsite_input.text() - self.loginWithCredentials(url,None,None) - - def checkUrl(self, url): - url = url.strip('/ ') - - if not url: - self.setError() - 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: - self.setError( - 'The server URL set in Templates could not be reached.' - ) - return - - - if ( - result.status_code != 200 or 'FTRACK_VERSION' not in result.headers - ): - self.setError( - '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: - self.setError( - '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: - self.setError( - 'The server URL you provided could not be reached.' - ) - return - - - if ( - result.status_code != 200 or 'FTRACK_VERSION' not in result.headers - ): - self.setError( - '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) - return - - verification = credentials._check_credentials(user, api) - - if verification is True: - credentials._save_credentials(username, apiKey) - credentials._set_env(username, apiKey) - self._close_widget() - - - def _close_widget(self): - sys.exit(app.exec_()) - - -class Login_Dialog(Login_Dialog_ui): - def __init__(self): - super(Login_Dialog, self).__init__() - - -def getApp(): - return QtWidgets.QApplication(sys.argv) - -def run_login(): - app = getApp() - ui = Login_Dialog() - ui.show() - sys.exit(app.exec_()) diff --git a/pype/ftrack/__init__.py b/pype/ftrack/__init__.py index e69de29bb2..8b13789179 100644 --- a/pype/ftrack/__init__.py +++ b/pype/ftrack/__init__.py @@ -0,0 +1 @@ + diff --git a/pype/ftrack/credentials.py b/pype/ftrack/credentials.py index 74b0b298ab..3efad41997 100644 --- a/pype/ftrack/credentials.py +++ b/pype/ftrack/credentials.py @@ -7,6 +7,10 @@ import appdirs config_path = os.path.normpath(appdirs.user_data_dir('pype-app','pype')) config_name = 'ftrack_cred.toml' fpath = os.path.join(config_path, config_name) +folder = os.path.dirname(fpath) + +if not os.path.isdir(folder): + os.makedirs(folder) def _get_credentials(): @@ -53,9 +57,9 @@ def _check_credentials(username=None, apiKey=None): try: session = ftrack_api.Session() + session.close() except Exception as e: print(e) return False - session.close() return True diff --git a/pype/ftrack/login_dialog.py b/pype/ftrack/login_dialog.py index f6e3b33478..4dd8a8dca3 100644 --- a/pype/ftrack/login_dialog.py +++ b/pype/ftrack/login_dialog.py @@ -1,38 +1,205 @@ import sys +import os import requests -from PyQt5 import QtCore, QtWidgets +from PyQt5 import QtCore, QtGui, QtWidgets from app import style from . import credentials, login_tools -class Login_Dialog(QtCore.QObject): +class Login_Dialog_ui(QtWidgets.QWidget): + + SIZE_W = 300 + SIZE_H = 230 loginSignal = QtCore.pyqtSignal(object, object, object) _login_server_thread = None + inputs = [] + buttons = [] + labels = [] def __init__(self): + super().__init__() self.loginSignal.connect(self.loginWithCredentials) self._translate = QtCore.QCoreApplication.translate - def run(self): + self.font = QtGui.QFont() + self.font.setFamily("DejaVu Sans Condensed") + self.font.setPointSize(9) + self.font.setBold(True) + self.font.setWeight(50) + self.font.setKerning(True) + + self.resize(self.SIZE_W, self.SIZE_H) + self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) + self.setStyleSheet(style.load_stylesheet()) + + self.setLayout(self._main()) + self.setWindowTitle('FTrack Login') + + self.show() + + def _main(self): + self.main = QtWidgets.QVBoxLayout() + self.main.setObjectName("main") + + self.form = QtWidgets.QFormLayout() + self.form.setContentsMargins(10, 15, 10, 5) + self.form.setObjectName("form") + + self.ftsite_label = QtWidgets.QLabel("FTrack URL:") + self.ftsite_label.setFont(self.font) + self.ftsite_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.ftsite_label.setTextFormat(QtCore.Qt.RichText) + self.ftsite_label.setObjectName("user_label") + + self.ftsite_input = QtWidgets.QLineEdit() + self.ftsite_input.setEnabled(True) + self.ftsite_input.setFrame(True) + self.ftsite_input.setEnabled(False) + self.ftsite_input.setReadOnly(True) + self.ftsite_input.setObjectName("ftsite_input") + + self.user_label = QtWidgets.QLabel("Username:") + self.user_label.setFont(self.font) + self.user_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.user_label.setTextFormat(QtCore.Qt.RichText) + self.user_label.setObjectName("user_label") + + self.user_input = QtWidgets.QLineEdit() + self.user_input.setEnabled(True) + self.user_input.setFrame(True) + self.user_input.setObjectName("user_input") + self.user_input.setPlaceholderText(self._translate("main","user.name")) + self.user_input.textChanged.connect(self._user_changed) + + self.api_label = QtWidgets.QLabel("API Key:") + self.api_label.setFont(self.font) + self.api_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.api_label.setTextFormat(QtCore.Qt.RichText) + self.api_label.setObjectName("api_label") + + self.api_input = QtWidgets.QLineEdit() + self.api_input.setEnabled(True) + self.api_input.setFrame(True) + self.api_input.setObjectName("api_input") + self.api_input.setPlaceholderText(self._translate("main","e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")) + self.api_input.textChanged.connect(self._api_changed) + + self.error_label = QtWidgets.QLabel("") + self.error_label.setFont(self.font) + self.error_label.setTextFormat(QtCore.Qt.RichText) + self.error_label.setObjectName("error_label") + self.error_label.setWordWrap(True); + self.error_label.hide() + + self.form.addRow(self.ftsite_label, self.ftsite_input) + self.form.addRow(self.user_label, self.user_input) + self.form.addRow(self.api_label,self.api_input) + self.form.addRow(self.error_label) + + self.btnGroup = QtWidgets.QHBoxLayout() + self.btnGroup.addStretch(1) + self.btnGroup.setObjectName("btnGroup") + + self.btnEnter = QtWidgets.QPushButton("Login") + self.btnEnter.setToolTip('Set Username and API Key with entered values') + self.btnEnter.clicked.connect(self.enter_credentials) + + self.btnClose = QtWidgets.QPushButton("Close") + self.btnClose.setToolTip('Close this window') + self.btnClose.clicked.connect(self._close_widget) + + self.btnFtrack = QtWidgets.QPushButton("Ftrack") + self.btnFtrack.setToolTip('Open browser for Login to Ftrack') + self.btnFtrack.clicked.connect(self.open_ftrack) + + self.btnGroup.addWidget(self.btnFtrack) + self.btnGroup.addWidget(self.btnEnter) + self.btnGroup.addWidget(self.btnClose) + + self.main.addLayout(self.form) + self.main.addLayout(self.btnGroup) + + self.inputs.append(self.api_input) + self.inputs.append(self.user_input) + self.inputs.append(self.ftsite_input) + + self.enter_site() + return self.main + + def enter_site(self): try: url = os.getenv('FTRACK_SERVER') - return - except: - print("Environment variable 'FTRACK_SERVER' is not set.") + newurl = self.checkUrl(url) - self.url = checkUrl(url) - self.open_ftrack() + if newurl is None: + self.btnEnter.setEnabled(False) + self.btnFtrack.setEnabled(False) + for input in self.inputs: + input.setEnabled(False) + newurl = url + + self.ftsite_input.setText(newurl) + + except Exception as e: + self.setError("FTRACK_SERVER is not set in templates") + self.setError(str(e)) + self.btnEnter.setEnabled(False) + self.btnFtrack.setEnabled(False) + for input in self.inputs: + input.setEnabled(False) + + def setError(self, msg): + self.error_label.setText(msg) + self.error_label.show() + + def _user_changed(self): + self.user_input.setStyleSheet("") + + def _api_changed(self): + self.api_input.setStyleSheet("") + + def _invalid_input(self,entity): + entity.setStyleSheet("border: 1px solid red;") + + def enter_credentials(self): + user = self.user_input.text().strip() + api = self.api_input.text().strip() + msg = "You didn't enter " + missing = [] + if user == "": + missing.append("Username") + self._invalid_input(self.user_input) + + if api == "": + missing.append("API Key") + self._invalid_input(self.api_input) + + if len(missing) > 0: + self.setError("{0} {1}".format(msg, " and ".join(missing))) + return + + verification = credentials._check_credentials(user, api) + + if verification: + credentials._save_credentials(username, apiKey) + credentials._set_env(username, apiKey) + self._close_widget() + else: + self._invalid_input(self.user_input) + self._invalid_input(self.api_input) + self.setError("We're unable to connect to Ftrack with these credentials") def open_ftrack(self): - self.loginWithCredentials(self.url,None,None) + url = self.ftsite_input.text() + self.loginWithCredentials(url,None,None) def checkUrl(self, url): url = url.strip('/ ') if not url: - print("Url is empty!") + self.setError() return if not 'http' in url: @@ -46,23 +213,26 @@ class Login_Dialog(QtCore.QObject): 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.') + self.setError( + '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.') + self.setError( + '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( + self.setError( 'You need to specify a valid server URL, ' 'for example https://server-name.ftrackapp.com' ) @@ -79,14 +249,18 @@ class Login_Dialog(QtCore.QObject): 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.') + self.setError( + '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.') + self.setError( + '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. @@ -102,15 +276,28 @@ class Login_Dialog(QtCore.QObject): self._login_server_thread.start(url) return - verification = credentials._check_credentials(user, api) + verification = credentials._check_credentials(username, apiKey) if verification is True: credentials._save_credentials(username, apiKey) credentials._set_env(username, apiKey) + self._close_widget() + def _close_widget(self): + self.close() + + +class Login_Dialog(Login_Dialog_ui): + def __init__(self): + super(Login_Dialog, self).__init__() + + +def getApp(): + return QtWidgets.QApplication(sys.argv) + def run_login(): - app = QtWidgets.QApplication(sys.argv) - applogin = Login_Dialog() - applogin.run() - sys.exit(app.exec_()) + app = getApp() + ui = Login_Dialog() + ui.show() + app.exec_() diff --git a/pype/ftrack/login_dialog_noui.py b/pype/ftrack/login_dialog_noui.py new file mode 100644 index 0000000000..5df2c62daf --- /dev/null +++ b/pype/ftrack/login_dialog_noui.py @@ -0,0 +1,120 @@ +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 11f28fcfa11fdb92f4af7bcffd666d27f187ed0a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 13 Nov 2018 17:51:57 +0100 Subject: [PATCH 13/16] Added json.dumps to sync to avalon improvement thumb to child --- pype/ftrack/actions/action_syncToAvalon.py | 5 ++++- pype/ftrack/actions/action_thumbToChildern.py | 5 +++-- pype/ftrack/actions/action_thumbToParent.py | 20 +++++++++++++------ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pype/ftrack/actions/action_syncToAvalon.py b/pype/ftrack/actions/action_syncToAvalon.py index 3808f5c8ff..f8c6275ada 100644 --- a/pype/ftrack/actions/action_syncToAvalon.py +++ b/pype/ftrack/actions/action_syncToAvalon.py @@ -5,6 +5,7 @@ import argparse import logging import os import ftrack_api +import json from ftrack_action_handler import BaseAction from avalon import io, inventory, lib @@ -160,7 +161,9 @@ class SyncToAvalon(BaseAction): job = session.create('Job', { 'user': user, 'status': 'running', - 'data': {'description': 'Synch Ftrack to Avalon.'} + 'data': json.dumps({ + 'description': 'Synch Ftrack to Avalon.' + }) }) try: diff --git a/pype/ftrack/actions/action_thumbToChildern.py b/pype/ftrack/actions/action_thumbToChildern.py index 54974c22d6..52d31ee4e5 100644 --- a/pype/ftrack/actions/action_thumbToChildern.py +++ b/pype/ftrack/actions/action_thumbToChildern.py @@ -24,7 +24,7 @@ class ThumbToChildren(BaseAction): def discover(self, session, entities, event): ''' Validation ''' - if (len(entities) <= 0 or entities[0].entity_type in ['Project']): + if (len(entities) != 1 or entities[0].entity_type in ['Project']): return False return True @@ -53,11 +53,12 @@ class ThumbToChildren(BaseAction): # inform the user that the job is done job['status'] = 'done' - session.commit() except: # fail the job if something goes wrong job['status'] = 'failed' raise + finally: + session.commit() return { 'success': True, diff --git a/pype/ftrack/actions/action_thumbToParent.py b/pype/ftrack/actions/action_thumbToParent.py index 82954ae0e5..ca9cd09aee 100644 --- a/pype/ftrack/actions/action_thumbToParent.py +++ b/pype/ftrack/actions/action_thumbToParent.py @@ -45,30 +45,38 @@ class ThumbToParent(BaseAction): try: for entity in entities: + parent = None + thumbid = None if entity.entity_type.lower() == 'assetversion': try: parent = entity['task'] except: par_ent = entity['link'][-2] parent = session.get(par_ent['type'], par_ent['id']) - - elif entity.entity_type.lower() == 'task': - parent = entity['parent'] - + else: + try: + parent = entity['parent'] + except: + print("Durin Action 'Thumb to Parent' went something wrong") thumbid = entity['thumbnail_id'] if parent and thumbid: parent['thumbnail_id'] = thumbid + status = 'done' + else: + status = 'failed' # inform the user that the job is done - job['status'] = 'done' - session.commit() + job['status'] = status or 'done' except: # fail the job if something goes wrong job['status'] = 'failed' raise + finally: + session.commit() + return { 'success': True, 'message': 'Created job for updating thumbnails!' From a67b670b37a1d8dab49a87100e4265f8694bffb6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 13 Nov 2018 18:41:28 +0100 Subject: [PATCH 14/16] ftrackRun.py was added here --- pype/ftrack/ftrackRun.py | 56 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 pype/ftrack/ftrackRun.py diff --git a/pype/ftrack/ftrackRun.py b/pype/ftrack/ftrackRun.py new file mode 100644 index 0000000000..245f7f2b46 --- /dev/null +++ b/pype/ftrack/ftrackRun.py @@ -0,0 +1,56 @@ +import sys +import os +import argparse + +from app.lib.utils import forward +from pype.ftrack import credentials, login_dialog as login_dialog + +# Validation if alredy logged into Ftrack +def validate(): + validation = False + cred = credentials._get_credentials() + if 'username' in cred and 'apiKey' in cred: + validation = credentials._check_credentials( + cred['username'], + cred['apiKey'] + ) + if validation is False: + login_dialog.run_login() + else: + login_dialog.run_login() + + validation = credentials._check_credentials() + if not validation: + print("We are unable to connect to Ftrack") + sys.exit() + +# Entered arguments +parser = argparse.ArgumentParser() +parser.add_argument("--actionserver", action="store_true", + help="launch action server for ftrack") +parser.add_argument("--eventserver", action="store_true", + help="launch action server for ftrack") +parser.add_argument("--logout", action="store_true", + help="launch action server for ftrack") + +kwargs, args = parser.parse_known_args() + +if kwargs.logout: + credentials._clear_credentials() + sys.exit() +else: + validate() + +if kwargs.eventserver: + fname = os.path.join(os.environ["FTRACK_ACTION_SERVER"], "eventServer.py") + returncode = forward([ + sys.executable, "-u", fname + ]) + +else: + fname = os.path.join(os.environ["FTRACK_ACTION_SERVER"], "actionServer.py") + returncode = forward([ + sys.executable, "-u", fname + ]) + +sys.exit(returncode) From 9967d0d5f6bdb75d43b7c7941c3057b1cd69e1f7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 13 Nov 2018 18:50:15 +0100 Subject: [PATCH 15/16] error label changed --- pype/ftrack/login_dialog.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pype/ftrack/login_dialog.py b/pype/ftrack/login_dialog.py index 4dd8a8dca3..c39602cd23 100644 --- a/pype/ftrack/login_dialog.py +++ b/pype/ftrack/login_dialog.py @@ -144,7 +144,6 @@ class Login_Dialog_ui(QtWidgets.QWidget): except Exception as e: self.setError("FTRACK_SERVER is not set in templates") - self.setError(str(e)) self.btnEnter.setEnabled(False) self.btnFtrack.setEnabled(False) for input in self.inputs: @@ -199,7 +198,7 @@ class Login_Dialog_ui(QtWidgets.QWidget): url = url.strip('/ ') if not url: - self.setError() + self.setError("There is no URL set in Templates") return if not 'http' in url: From 73b7560519c6955663db0609af9e92170a60fa47 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 14 Nov 2018 12:23:20 +0100 Subject: [PATCH 16/16] Changed HTML view after login --- pype/ftrack/login_dialog.py | 2 +- pype/ftrack/login_tools.py | 36 ++++++++++++++++++++++-------------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/pype/ftrack/login_dialog.py b/pype/ftrack/login_dialog.py index c39602cd23..622cfc21b5 100644 --- a/pype/ftrack/login_dialog.py +++ b/pype/ftrack/login_dialog.py @@ -188,7 +188,7 @@ class Login_Dialog_ui(QtWidgets.QWidget): else: self._invalid_input(self.user_input) self._invalid_input(self.api_input) - self.setError("We're unable to connect to Ftrack with these credentials") + self.setError("We're unable to sign in to Ftrack with these credentials") def open_ftrack(self): url = self.ftsite_input.text() diff --git a/pype/ftrack/login_tools.py b/pype/ftrack/login_tools.py index 3c0f430c9e..719e6bac37 100644 --- a/pype/ftrack/login_tools.py +++ b/pype/ftrack/login_tools.py @@ -28,31 +28,39 @@ class LoginServerHandler(BaseHTTPRequestHandler): message = """ - -

Sign in to ftrack connect was successful

-

- You signed in with username {0} and can now - close this window. -

- + +

Sign in to Ftrack was successful

+

+ You signed in with username {0}. +

+

+ You can close this window now. +

+ """.format(api_user) else: