Snippets

ftrack Action - Transfer components

Updated by Mattias Lagergren

File README.rst Modified

  • Ignore whitespace
  • Hide word diff
 have been copied to the target location and are ready for tomorrow.
 
 For more information on using ftrack for collaboration between locations, see
-the `documentation <http://ftrack.rtd.ftrack.com/en/latest/using/locations/index.html>`.
+the `documentation <http://help.ftrack.com/developing-with-ftrack/key-concepts/locations>`.
 
 Using the action
 ================
 Available on
 ============
 
-* Tasks (including encapsulating folders such as shots)
-* Versions
+* Project
+* Objects (Folder, Shot, Task, etc.)
+* Version
+* Component
 
 Running the action
 ==================
 
+Running the action
+==================
+
+Install the ftrack-action-handler:
+
+.. code::
+
+    pip install git+https://bitbucket.org/ftrack/ftrack-action-handler.git
+
+.. tip::
+
+    You can use `virtualenv <https://virtualenv.pypa.io/en/stable/>`_ or similar
+    solutions to avoid installing packages into your system's Python environment.
+
 Start the listener from the terminal using the following command:
 
 .. code::
 If you wish to see debugging information, set the verbosity level by appending
 ``-v debug`` to the command.
 
-For more information, see the 
-`documentation on using actions <http://ftrack.rtd.ftrack.com/en/latest/using/actions.html>`_.
+Reade more
+==========
+
+For more information in our documentation:
+
+* `Using actions <http://ftrack.rtd.ftrack.com/en/latest/using/actions.html>`_.
+* `Action handler <http://ftrack-action-handler.rtd.ftrack.com/en/latest/index.html>`_.

File transfer_components_action.py Modified

  • Ignore whitespace
  • Hide word diff
 # :coding: utf-8
 # :copyright: Copyright (c) 2015 ftrack
+
+import json
 import sys
 import argparse
 import logging
 import threading
+import collections
+
+import ftrack_api
+import ftrack_api.exception
+import ftrack_action_handler.action
 
-import ftrack
+SUPPORTED_ENTITY_TYPES = (
+    'AssetVersion', 'TypedContext', 'Project', 'Component'
+)
 
 
 def async(fn):
     return wrapper
 
 
-class TransferComponentsAction(ftrack.Action):
+def get_filter_string(entity_ids):
+    '''Return a comma separated string of quoted ids from *entity_ids* list.'''
+    return ', '.join(
+        '"{0}"'.format(entity_id) for entity_id in entity_ids
+    )
+
+
+class TransferComponentsAction(ftrack_action_handler.action.BaseAction):
     '''Action to transfer components between locations.'''
 
     #: Action identifier.
     #: Action label.
     label = 'Transfer component(s)'
 
-    def validateSelection(self, selection):
-        '''Return if *selection* is valid.'''
+    #: Action description.
+    description = 'Transfer component(s) between locations.'
+
+    def validate_entities(self, entities):
+        '''Return if *entities* is valid.'''
         if (
-            len(selection) >= 1 and
-            any(
-                True for item in selection
-                if item.get('entityType') in ('assetversion', 'task', 'show')
+            len(entities) >= 1 and
+            all(
+                [
+                    entity_type in SUPPORTED_ENTITY_TYPES
+                    for entity_type, _ in entities
+                ]
             )
         ):
             self.logger.info('Selection is valid')
             self.logger.info('Selection is _not_ valid')
             return False
 
-    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
-
-        return super(TransferComponentsAction, self).discover(event)
-
-
-    def getVersionsInSelection(self, selection):
-        '''Return list of versions in *selection*.'''
-        versions = []
-        for item in selection:
-            self.logger.info(
-                'Looking for versions on entity ({0}, {1})'.format(item['entityId'], item['entityType'])
+    def discover(self, session, entities, event):
+        '''Return True if action is valid.'''
+        self.logger.info(
+            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*.'''
+        component_queries = []
+        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']))
-                continue
-
-            entity = None
-            if item['entityType'] == 'show':
-                entity = ftrack.Project(item['entityId'])
-            elif item['entityType'] == 'task':
-                entity = ftrack.Task(item['entityId'])
-
-            if not entity:
-                continue
-
-            assets = entity.getAssets(includeChildren=True)
-            self.logger.info('Found {0} assets on entity'.format(len(assets)))
-            for asset in assets:
-                assetVersions = asset.getVersions()
-                self.logger.info(
-                    '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 '
+                'in ({0})'.format(
+                    get_filter_string(entity_groups['TypedContext'])
                 )
-                versions.extend(assetVersions)
+            )
 
-        self.logger.info('Found {0} versions in selection'.format(len(versions)))
-        return 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'])
+                )
+            )
 
-        components = []
-        for version in versions:
-            self.logger.info('Looking for components on version {0}'.format(version.getId()))
-            components.extend(version.getComponents(location=location))
+        components = set()
+        for query_string in component_queries:
+            components.update(
+                session.query(
+                    '{0} and component_locations.location_id is "{1}"'.format(
+                        query_string, location['id']
+                    )
+                ).all()
+            )
 
-        self.logger.info('Found {0} components in selection'.format(len(components)))
-        return components
+        self.logger.info(
+            'Found {0} components in selection'.format(len(components))
+        )
+        return list(components)
 
     @async
-    def transferComponents(
-        self, selection, sourceLocation, targetLocation,
-        userId=None,
-        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-
-        related errors.
+        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', {
+            'user_id': user_id,
+            'status': 'running',
+            'data': json.dumps({
+                'description': 'Transfer components (Gathering...)'
+            })
+        })
+        session.commit()
         try:
-            components = self.getComponentsInLocation(selection, sourceLocation)
+            components = self.get_components_in_location(
+                session, entities, source_location
+            )
             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))
+                self.logger.info(
+                    'Transferring component ({0} of {1})'.format(index, amount)
+                )
+                job['data'] = json.dumps({
+                    'description': 'Transfer components ({0} of {1})'.format(
+                        index, amount
+                    )
+                })
+                session.commit()
 
                 try:
-                    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:
+                    self.logger.info(
+                        'Component ({}) already in target location'.format(
+                            component
+                        )
+                    )
+                except ftrack_api.exception.ComponentNotInLocationError:
+                    if (
+                        ignore_component_not_in_location or
+                        ignore_location_errors
+                    ):
+                        self.logger.exception(
+                            'Failed to add component to location'
+                        )
                     else:
                         raise
-                except ftrack.LocationError:
-                    if ignoreLocationErrors:
-                        self.logger.exception('Failed to add component to location')
+                except ftrack_api.exception.LocationError:
+                    if ignore_location_errors:
+                        self.logger.exception(
+                            'Failed to add component to location'
+                        )
                     else:
                         raise
 
-            job.setStatus('done')
-            self.logger.info('Transfer complete ({0} components)'.format(amount))
-
-        except Exception:
-            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:
-                return {
-                    'success': False,
-                    'message': 'Source and target locations are the same.'
-                }
-
-            ignoreComponentNotInLocation = (
-                values.get('ignore_component_not_in_location') == 'true'
-            )
-            ignoreLocationErrors = (
-                values.get('ignore_location_errors') == 'true'
-            )
+            job['status'] = 'done'
+            session.commit()
 
             self.logger.info(
-                'Transferring components from {0} to {1}'.format(sourceLocation, targetLocation)
-            )
-            self.transferComponents(
-                selection,
-                sourceLocation,
-                targetLocation,
-                userId=userId,
-                ignoreComponentNotInLocation=ignoreComponentNotInLocation,
-                ignoreLocationErrors=ignoreLocationErrors
+                'Transfer complete ({0} components)'.format(amount)
             )
-            return {
-                'success': True,
-                'message': 'Transferring components...'
-            }
 
-        allLocations = [
-            {
-                'label': location.get('name'),
-                'value': location.get('id')
-            }
-            for location in ftrack.getLocations(excludeInaccessible=True)
-        ]
+        except BaseException:
+            self.logger.exception('Transfer failed')
+            session.rollback()
+            job['status'] = 'failed'
+            session.commit()
+
+    def launch(self, session, entities, event):
+        '''Launch edit meta data action.'''
+        self.logger.info(
+            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:
             return {
                 'success': False,
-                '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'
+        )
+
+        self.logger.info(
+            'Transferring components from {0} to {1}'
+            .format(source_location, target_location)
+        )
+        user_id = event['source']['user']['id']
+        self.transfer_components(
+            entities,
+            source_location,
+            target_location,
+            user_id=user_id,
+            ignore_component_not_in_location=ignore_component_not_in_location,
+            ignore_location_errors=ignore_location_errors
+        )
         return {
-            'items': [
+            'success': True,
+            'message': 'Transferring components...'
+        }
+
+    def interface(self, session, entities, event):
+        '''Return interface.'''
+        values = event['data'].get('values', {})
+
+        if not values:
+            locations = filter(
+                lambda location: location.accessor,
+                session.query('select name, label from Location').all()
+            )
+            # Sort by priority.
+            locations = sorted(
+                locations, key=lambda location: location.priority
+            )
+            locations_options = [
+                {
+                    'label': location['label'] or location['name'],
+                    'value': location['id']
+                }
+                for location in locations
+            ]
+            return [
                 {
                     'value': 'Transfer components between locations',
                     'type': 'label'
                     'label': 'Source location',
                     'type': 'enumerator',
                     'name': 'from_location',
-                    'value': allLocations[0]['value'],
-                    'data': allLocations
+                    'value': locations_options[0]['value'],
+                    'data': locations_options
                 }, {
                     'label': 'Target location',
                     'type': 'enumerator',
                     'name': 'to_location',
-                    'value': allLocations[1]['value'],
-                    'data': allLocations
+                    'value': locations_options[1]['value'],
+                    'data': locations_options
                 }, {
                     'value': '---',
                     'type': 'label'
                     ]
                 }
             ]
-        }
 
 
-def register(registry, **kw):
-    '''Register action. Called when used as an event plugin.'''
-    logger = logging.getLogger(
-        'transfer-components'
-    )
+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):
-        logger.debug(
-            'Not subscribing plugin as passed argument {0!r} is not an '
-            'ftrack.Registry instance.'.format(registry)
-        )
+    if not isinstance(session, ftrack_api.session.Session):
         return
 
-    action = TransferComponentsAction()
-    action.register()
+    action_handler = TransferComponentsAction(session)
+    action_handler.register()
 
 
 def main(arguments=None):
     # Set up basic logging
     logging.basicConfig(level=loggingLevels[namespace.verbosity])
 
-    # Subscribe to action.
-    ftrack.setup()
-    action = TransferComponentsAction()
-    action.register()
+    session = ftrack_api.Session()
+    register(session)
 
     # Wait for events
-    ftrack.EVENT_HUB.wait()
+    logging.info(
+        'Registered actions and listening for events. Use Ctrl-C to abort.'
+    )
+    session.event_hub.wait()
 
 
 if __name__ == '__main__':
Updated by Lucas Correia

File transfer_components_action.py Modified

  • Ignore whitespace
  • Hide word diff
         )
         return
 
+    action = TransferComponentsAction()
+    action.register()
+
 
 def main(arguments=None):
     '''Set up logging and register action.'''
Updated by Lucas Correia

File transfer_components_action.py Modified

  • Ignore whitespace
  • Hide word diff
         }
 
 
+def register(registry, **kw):
+    '''Register action. Called when used as an event plugin.'''
+    logger = logging.getLogger(
+        'transfer-components'
+    )
+
+    # Validate that registry is an instance of ftrack.Registry. If not,
+    # assume that register is being called from a new or incompatible API and
+    # return without doing anything.
+    if not isinstance(registry, ftrack.Registry):
+        logger.debug(
+            'Not subscribing plugin as passed argument {0!r} is not an '
+            'ftrack.Registry instance.'.format(registry)
+        )
+        return
+
+
 def main(arguments=None):
     '''Set up logging and register action.'''
     if arguments is None:
Updated by Lucas Correia

File transfer_components_action.py Modified

  • Ignore whitespace
  • Hide word diff
         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'])
+            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:
                 return {
                     'success': False,
                 }
 
             ignoreComponentNotInLocation = (
-                event['data']['values'].get('ignore_component_not_in_location') == 'true'
+                values.get('ignore_component_not_in_location') == 'true'
             )
             ignoreLocationErrors = (
-                event['data']['values'].get('ignore_location_errors') == 'true'
+                values.get('ignore_location_errors') == 'true'
             )
 
             self.logger.info(
                     'label': 'Ignore missing',
                     'type': 'enumerator',
                     'name': 'ignore_component_not_in_location',
-                    'value': 'No',
+                    'value': 'false',
                     'data': [
                         {'label': 'Yes', 'value': 'true'},
                         {'label': 'No', 'value': 'false'}
                     'label': 'Ignore errors',
                     'type': 'enumerator',
                     'name': 'ignore_location_errors',
-                    'value': 'No',
+                    'value': 'false',
                     'data': [
                         {'label': 'Yes', 'value': 'true'},
                         {'label': 'No', 'value': 'false'}
Updated by Lucas Correia

File README.rst Modified

  • Ignore whitespace
  • Hide word diff
 Transfer components action
 **************************
 
+The *transfer components* action can be used to move components (files) between
+two locations.
+
+For example, consider the following scenario. You are working with a *shot*, 
+*SH_010*, on which a few different versions has been published to the network
+storage at the office. Tomorrow, you will be working from home and will need
+access to those versions while away from the office. To achieve this you select
+*SH_010* in the spreadsheet and run the action. Select the *Network storage* 
+location as the *Source location* and *My local files* as the target location
+and begin transferring the components. The action will create a job and report
+back with feedback as the transfer progresses. Once completed, the files will
+have been copied to the target location and are ready for tomorrow.
+
+For more information on using ftrack for collaboration between locations, see
+the `documentation <http://ftrack.rtd.ftrack.com/en/latest/using/locations/index.html>`.
+
+Using the action
+================
+
+Navigate to a project in the web interface and select a few items
+in the spreadsheet and select `Actions` from the context menu. Click 
+on `Transfer component(s)` and select source and target locations.
+
+The action will look for any versions published on the items selected 
+(including descendants) and gather all components on those versions. The action
+will then continue by adding each of the components to the target location.
+
+Available on
+============
+
+* Tasks (including encapsulating folders such as shots)
+* Versions
+
 Running the action
 ==================
 
 
 For more information, see the 
 `documentation on using actions <http://ftrack.rtd.ftrack.com/en/latest/using/actions.html>`_.
-
-Using the action
-================
-
-Navigate to a project in the web interface and select a few items
-in the spreadsheet and select `Actions` from the context menu. Click 
-on `Transfer component(s)` and select source and target locations.
-
-The action will look for any versions published on the items selected 
-(including descendants) and gather all components on those versions. The action
-will then continue by adding each of the components to the target location.
- 
  1. 1
  2. 2
HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.