+# :copyright: Copyright (c) 2015 ftrack
+ '''Run *fn* asynchronously.'''
+ def wrapper(*args, **kwargs):
+ thread = threading.Thread(target=fn, args=args, kwargs=kwargs)
+class TransferComponentsAction(ftrack.Action):
+ '''Action to transfer components between locations.'''
+ identifier = 'transfer-components'
+ label = 'Transfer component(s)'
+ def validateSelection(self, selection):
+ '''Return if *selection* is valid.'''
+ len(selection) >= 1 and
+ True for item in selection
+ if item.get('entityType') in ('assetversion', 'task', 'show')
+ self.logger.info('Selection is valid')
+ self.logger.info('Selection is _not_ valid')
+ def discover(self, event):
+ '''Return action config.'''
+ selection = event['data'].get('selection', [])
+ self.logger.info(u'Discovering action with selection: {0}'.format(selection))
+ if not self.validateSelection(selection):
+ return super(TransferComponentsAction, self).discover(event)
+ def getVersionsInSelection(self, selection):
+ '''Return list of versions in *selection*.'''
+ 'Looking for versions on entity ({0}, {1})'.format(item['entityId'], item['entityType'])
+ if item['entityType'] == 'assetversion':
+ versions.append(ftrack.AssetVersion(item['entityId']))
+ if item['entityType'] == 'show':
+ entity = ftrack.Project(item['entityId'])
+ elif item['entityType'] == 'task':
+ entity = ftrack.Task(item['entityId'])
+ assets = entity.getAssets(includeChildren=True)
+ self.logger.info('Found {0} assets on entity'.format(len(assets)))
+ assetVersions = asset.getVersions()
+ 'Found {0} versions on asset {1}'.format(len(assetVersions), asset.getId())
+ versions.extend(assetVersions)
+ self.logger.info('Found {0} versions in selection'.format(len(versions)))
+ def getComponentsInLocation(self, selection, location):
+ '''Return list of components in *selection*.'''
+ versions = self.getVersionsInSelection(selection)
+ for version in versions:
+ self.logger.info('Looking for components on version {0}'.format(version.getId()))
+ components.extend(version.getComponents(location=location))
+ self.logger.info('Found {0} components in selection'.format(len(components)))
+ def transferComponents(
+ self, selection, sourceLocation, targetLocation,
+ ignoreComponentNotInLocation=False,
+ ignoreLocationErrors=False
+ '''Transfer components in *selection* from *sourceLocation* to *targetLocation*.
+ if *ignoreComponentNotInLocation*, ignore components missing in source
+ location. If *ignoreLocationErrors* is specified, ignore all locations-
+ Reports progress back to *userId* using a job.
+ job = ftrack.createJob('Transfer components (Gathering...)', 'running', user=userId)
+ components = self.getComponentsInLocation(selection, sourceLocation)
+ amount = len(components)
+ self.logger.info('Transferring {0} components'.format(amount))
+ for index, component in enumerate(components, start=1):
+ self.logger.info('Transferring component ({0} of {1})'.format(index, amount))
+ job.setDescription('Transfer components ({0} of {1})'.format(index, amount))
+ targetLocation.addComponent(component, manageData=True)
+ except ftrack.ComponentInLocationError:
+ self.logger.info('Component ({}) already in target location'.format(component))
+ except ftrack.ComponentNotInLocationError:
+ if ignoreComponentNotInLocation or ignoreLocationErrors:
+ self.logger.exception('Failed to add component to location')
+ except ftrack.LocationError:
+ if ignoreLocationErrors:
+ self.logger.exception('Failed to add component to location')
+ self.logger.info('Transfer complete ({0} components)'.format(amount))
+ self.logger.exception('Transfer failed')
+ job.setStatus('failed')
+ def launch(self, event):
+ '''Callback method for action.'''
+ selection = event['data'].get('selection', [])
+ userId = event['source']['user']['id']
+ self.logger.info(u'Launching action with selection: {0}'.format(selection))
+ if 'values' in event['data']:
+ sourceLocation = ftrack.Location(event['data']['values']['from_location'])
+ targetLocation = ftrack.Location(event['data']['values']['to_location'])
+ if sourceLocation == targetLocation:
+ 'message': 'Source and target locations are the same.'
+ ignoreComponentNotInLocation = (
+ event['data']['values'].get('ignore_component_not_in_location') == 'true'
+ ignoreLocationErrors = (
+ event['data']['values'].get('ignore_location_errors') == 'true'
+ 'Transferring components from {0} to {1}'.format(sourceLocation, targetLocation)
+ self.transferComponents(
+ ignoreComponentNotInLocation=ignoreComponentNotInLocation,
+ ignoreLocationErrors=ignoreLocationErrors
+ 'message': 'Transferring components...'
+ 'label': location.get('name'),
+ 'value': location.get('id')
+ for location in ftrack.getLocations(excludeInaccessible=True)
+ if len(allLocations) < 2:
+ self.transferComponents(selection, sourceLocation, targetLocation)
+ 'message': 'Did not find two accessible locations'
+ 'value': 'Transfer components between locations',
+ 'label': 'Source location',
+ 'name': 'from_location',
+ 'value': allLocations[0]['value'],
+ 'label': 'Target location',
+ 'value': allLocations[1]['value'],
+ 'label': 'Ignore missing',
+ 'name': 'ignore_component_not_in_location',
+ {'label': 'Yes', 'value': 'true'},
+ {'label': 'No', 'value': 'false'}
+ 'label': 'Ignore errors',
+ 'name': 'ignore_location_errors',
+ {'label': 'Yes', 'value': 'true'},
+ {'label': 'No', 'value': 'false'}
+def main(arguments=None):
+ '''Set up logging and register action.'''
+ parser = argparse.ArgumentParser()
+ # Allow setting of logging level from arguments.
+ logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
+ logging.ERROR, logging.CRITICAL
+ loggingLevels[logging.getLevelName(level).lower()] = level
+ help='Set the logging output verbosity.',
+ choices=loggingLevels.keys(),
+ namespace = parser.parse_args(arguments)
+ logging.basicConfig(level=loggingLevels[namespace.verbosity])
+ action = TransferComponentsAction()
+ ftrack.EVENT_HUB.wait()
+if __name__ == '__main__':
+ raise SystemExit(main(sys.argv[1:]))