# :copyright: Copyright (c) 2015 ftrack
+import ftrack_api.exception
+import ftrack_action_handler.action
+SUPPORTED_ENTITY_TYPES = (
+ 'AssetVersion', 'TypedContext', 'Project', 'Component'
-class TransferComponentsAction(ftrack.Action):
+def get_filter_string(entity_ids):
+ '''Return a comma separated string of quoted ids from *entity_ids* list.'''
+ '"{0}"'.format(entity_id) for entity_id in entity_ids
+class TransferComponentsAction(ftrack_action_handler.action.BaseAction):
'''Action to transfer components between locations.'''
label = 'Transfer component(s)'
- def validateSelection(self, selection):
- '''Return if *selection* is valid.'''
+ description = 'Transfer component(s) between locations.'
+ def validate_entities(self, entities):
+ '''Return if *entities* is valid.'''
- len(selection) >= 1 and
- True for item in selection
- if item.get('entityType') in ('assetversion', 'task', 'show')
+ entity_type in SUPPORTED_ENTITY_TYPES
+ for entity_type, _ in entities
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'])
+ def discover(self, session, entities, event):
+ '''Return True if action is valid.'''
+ u'Discovering action with entities: {0}'.format(entities)
+ return self.validate_entities(entities)
+ def get_components_in_location(self, session, entities, location):
+ '''Return list of components in *entities*.'''
+ entity_groups = collections.defaultdict(list)
+ for entity_type, entity_id in entities:
+ entity_groups[entity_type].append(entity_id)
+ if entity_groups['Project']:
+ component_queries.append(
+ 'Component where (version.asset.parent.project.id in ({0}) or '
+ 'version.asset.parent.id in ({0}))'.format(
+ get_filter_string(entity_groups['Project'])
- 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())
+ if entity_groups['TypedContext']:
+ component_queries.append(
+ 'Component where version.asset.parent.ancestors.id '
+ get_filter_string(entity_groups['TypedContext'])
- versions.extend(assetVersions)
- self.logger.info('Found {0} versions in selection'.format(len(versions)))
+ if entity_groups['AssetVersion']:
+ component_queries.append(
+ 'Component where version_id in ({0})'.format(
+ get_filter_string(entity_groups['AssetVersion'])
- def getComponentsInLocation(self, selection, location):
- '''Return list of components in *selection*.'''
- versions = self.getVersionsInSelection(selection)
+ if entity_groups['Component']:
+ component_queries.append(
+ 'Component where id in ({0})'.format(
+ get_filter_string(entity_groups['Component'])
- for version in versions:
- self.logger.info('Looking for components on version {0}'.format(version.getId()))
- components.extend(version.getComponents(location=location))
+ for query_string in component_queries:
+ '{0} and component_locations.location_id is "{1}"'.format(
+ query_string, location['id']
- self.logger.info('Found {0} components in selection'.format(len(components)))
+ 'Found {0} components in selection'.format(len(components))
+ return list(components)
- def transferComponents(
- self, selection, sourceLocation, targetLocation,
- ignoreComponentNotInLocation=False,
- ignoreLocationErrors=False
+ def transfer_components(
+ self, entities, source_location, target_location, user_id=None,
+ ignore_component_not_in_location=False, ignore_location_errors=False
- '''Transfer components in *selection* from *sourceLocation* to *targetLocation*.
+ '''Transfer components in *entities* from *source_location*.
+ if *ignore_component_not_in_location*, ignore components missing in
+ source location. If *ignore_location_errors* is specified, ignore all
+ locations-related errors.
- if *ignoreComponentNotInLocation*, ignore components missing in source
- location. If *ignoreLocationErrors* is specified, ignore all locations-
+ Reports progress back to *user_id* using a job.
- Reports progress back to *userId* using a job.
- job = ftrack.createJob('Transfer components (Gathering...)', 'running', user=userId)
+ session = ftrack_api.Session(
+ auto_connect_event_hub=False
+ job = session.create('Job', {
+ 'description': 'Transfer components (Gathering...)'
- components = self.getComponentsInLocation(selection, sourceLocation)
+ components = self.get_components_in_location(
+ session, entities, source_location
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))
+ 'Transferring component ({0} of {1})'.format(index, amount)
+ job['data'] = json.dumps({
+ 'description': 'Transfer components ({0} of {1})'.format(
- 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')
+ target_location.add_component(
+ component, source=source_location
+ except ftrack_api.exception.ComponentInLocationError:
+ 'Component ({}) already in target location'.format(
+ except ftrack_api.exception.ComponentNotInLocationError:
+ ignore_component_not_in_location or
+ 'Failed to add component to location'
- except ftrack.LocationError:
- if ignoreLocationErrors:
- self.logger.exception('Failed to add component to location')
+ except ftrack_api.exception.LocationError:
+ if ignore_location_errors:
+ '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']:
- values = event['data']['values']
- self.logger.info(u'Received values: {0}'.format(values))
- sourceLocation = ftrack.Location(values['from_location'])
- targetLocation = ftrack.Location(values['to_location'])
- if sourceLocation == targetLocation:
- 'message': 'Source and target locations are the same.'
- ignoreComponentNotInLocation = (
- values.get('ignore_component_not_in_location') == 'true'
- ignoreLocationErrors = (
- values.get('ignore_location_errors') == 'true'
- 'Transferring components from {0} to {1}'.format(sourceLocation, targetLocation)
- self.transferComponents(
- ignoreComponentNotInLocation=ignoreComponentNotInLocation,
- ignoreLocationErrors=ignoreLocationErrors
+ 'Transfer complete ({0} components)'.format(amount)
- 'message': 'Transferring components...'
- 'label': location.get('name'),
- 'value': location.get('id')
- for location in ftrack.getLocations(excludeInaccessible=True)
+ self.logger.exception('Transfer failed')
+ job['status'] = 'failed'
+ def launch(self, session, entities, event):
+ '''Launch edit meta data action.'''
+ u'Launching action with selection: {0}'.format(entities)
+ values = event['data']['values']
+ self.logger.info(u'Received values: {0}'.format(values))
- if len(allLocations) < 2:
- self.transferComponents(selection, sourceLocation, targetLocation)
+ source_location = session.get('Location', values['from_location'])
+ target_location = session.get('Location', values['to_location'])
+ if source_location == target_location:
- 'message': 'Did not find two accessible locations'
+ 'message': 'Source and target locations are the same.'
+ ignore_component_not_in_location = (
+ values.get('ignore_component_not_in_location') == 'true'
+ ignore_location_errors = (
+ values.get('ignore_location_errors') == 'true'
+ 'Transferring components from {0} to {1}'
+ .format(source_location, target_location)
+ user_id = event['source']['user']['id']
+ self.transfer_components(
+ ignore_component_not_in_location=ignore_component_not_in_location,
+ ignore_location_errors=ignore_location_errors
+ 'message': 'Transferring components...'
+ def interface(self, session, entities, event):
+ '''Return interface.'''
+ values = event['data'].get('values', {})
+ lambda location: location.accessor,
+ session.query('select name, label from Location').all()
+ locations, key=lambda location: location.priority
+ 'label': location['label'] or location['name'],
+ 'value': location['id']
+ for location in locations
'value': 'Transfer components between locations',
'label': 'Source location',
- 'value': allLocations[0]['value'],
+ 'value': locations_options[0]['value'],
+ 'data': locations_options
'label': 'Target location',
- 'value': allLocations[1]['value'],
+ 'value': locations_options[1]['value'],
+ 'data': locations_options
-def register(registry, **kw):
- '''Register action. Called when used as an event plugin.'''
- logger = logging.getLogger(
+def register(session, **kw):
+ '''Register plugin. Called when used as an plugin.'''
- # Validate that registry is an instance of ftrack.Registry. If not,
- # assume that register is being called from a new or incompatible API and
+ # 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(registry, ftrack.Registry):
- 'Not subscribing plugin as passed argument {0!r} is not an '
- 'ftrack.Registry instance.'.format(registry)
+ if not isinstance(session, ftrack_api.session.Session):
- action = TransferComponentsAction()
+ action_handler = TransferComponentsAction(session)
+ action_handler.register()
def main(arguments=None):
logging.basicConfig(level=loggingLevels[namespace.verbosity])
- action = TransferComponentsAction()
+ session = ftrack_api.Session()
- ftrack.EVENT_HUB.wait()
+ 'Registered actions and listening for events. Use Ctrl-C to abort.'
+ session.event_hub.wait()
if __name__ == '__main__':