Advertisement
Guest User

Untitled

a guest
Nov 12th, 2015
336
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 16.08 KB | None | 0 0
  1. # :coding: utf-8
  2. # :copyright: Copyright (c) 2015 ftrack
  3. import getpass
  4. import sys
  5. import argparse
  6. import logging
  7. import threading
  8.  
  9. import ftrack
  10. import os
  11.  
  12.  
  13. def async(fn):
  14.     """Run *fn* asynchronously."""
  15.     def wrapper(*args, **kwargs):
  16.         thread = threading.Thread(target=fn, args=args, kwargs=kwargs)
  17.         thread.start()
  18.     return wrapper
  19.  
  20.  
  21. class TransferComponentsAction(ftrack.Action):
  22.     """Action to transfer components between locations."""
  23.  
  24.     #: Action identifier.
  25.     identifier = 'transfer-components'
  26.  
  27.     #: Action label.
  28.     label = 'Transfer component(s)'
  29.  
  30.     # Get current user from OS environment
  31.     currentUser = getpass.getuser()
  32.  
  33.  
  34.     def register(self):
  35.         """
  36.        This overloads the base register() function to only subscribe to receive
  37.        certain events that have user information in them.
  38.  
  39.        Used to filter on users that initiated the action.
  40.        """
  41.  
  42.         self.logger.debug('register() called from: {0}'.format(__name__))
  43.  
  44.         # only subscribe/launch if this action's username match.
  45.         ftrack.EVENT_HUB.subscribe(
  46.             subscription='topic=ftrack.action.discover and source.user.username={0}'
  47.                 .format(self.currentUser),
  48.             callback=self.discover
  49.         )
  50.  
  51.         ftrack.EVENT_HUB.subscribe(
  52.             subscription='topic=ftrack.action.launch and data.actionIdentifier={0} '
  53.                          'and source.user.username={1}'
  54.                 .format(self.identifier, self.currentUser),
  55.             callback=self.launch
  56.         )
  57.  
  58.  
  59.     def validateSelection(self, selection):
  60.         """
  61.        Return if *selection* is valid.
  62.        This ensures only Asset Versions, Task view and Show tab are valid selections
  63.        """
  64.         if (
  65.             len(selection) >= 1 and
  66.             any(
  67.                 True for item in selection
  68.                 if item.get('entityType') in ('assetversion', 'task', 'show')
  69.             )
  70.         ):
  71.             self.logger.info('Selection is valid')
  72.             return True
  73.         else:
  74.             self.logger.info('Selection is _not_ valid')
  75.             return False
  76.  
  77.  
  78.     def discover(self, event):
  79.         """
  80.        This method is invoked when the user brings up the
  81.        Actions menu from the web UI.
  82.        """
  83.  
  84.         selection = event['data'].get('selection', [])
  85.         self.logger.info(u'Discovering action with selection: {0}'.format(selection))
  86.  
  87.         # Check that the selected entity is valid to run Action on
  88.         if not self.validateSelection(selection):
  89.             return
  90.  
  91.         return super(TransferComponentsAction, self).discover(event)
  92.  
  93.  
  94.     def getVersionsInSelection(self, selection):
  95.         """
  96.        This method returns a list of the ``ftrack.AssetVersion`` objects that
  97.        are associated with a *selection*.
  98.  
  99.        :param selection: ``list`` containing currently selected ftrack entities.
  100.        :return: ``list`` containing ftrack entity objects.
  101.        :rtype: ``list``
  102.        """
  103.  
  104.         versions = []
  105.  
  106.         # Iterate over selection and get the available versions
  107.         for item in selection:
  108.             self.logger.info(
  109.                 'Looking for versions on entity ({0}, {1})'
  110.                     .format(item['entityId'], item['entityType'])
  111.             )
  112.  
  113.             if item['entityType'] == 'assetversion':
  114.                 versions.append(ftrack.AssetVersion(item['entityId']))
  115.                 continue
  116.  
  117.             # Get a valid entity type
  118.             entity = None
  119.  
  120.             if item['entityType'] == 'show':
  121.                 entity = ftrack.Project(item['entityId'])
  122.  
  123.             elif item['entityType'] == 'task':
  124.                 entity = ftrack.Task(item['entityId'])
  125.  
  126.             if not entity:
  127.                 continue
  128.  
  129.             # Get all assetVersion containers on the entity to get Versions from
  130.             assets = entity.getAssets(includeChildren=True)
  131.             self.logger.info('Found {0} assets on entity'.format(len(assets)))
  132.  
  133.             # Grab each AssetVersion from each Asset
  134.             for asset in assets:
  135.                 assetVersions = asset.getVersions()
  136.                 self.logger.info(
  137.                     'Found {0} versions on asset {1}'.format(len(assetVersions), asset.getId())
  138.                 )
  139.                 versions.extend(assetVersions)
  140.  
  141.         self.logger.info('Found {0} versions in selection'.format(len(versions)))
  142.         return versions
  143.  
  144.  
  145.     def getComponentsInLocation(self, selection, location):
  146.         """
  147.        This method returns a ``list`` of components in *selection*.
  148.        """
  149.         versions = self.getVersionsInSelection(selection)
  150.  
  151.         components = []
  152.  
  153.         for version in versions:
  154.             self.logger.info('Looking for components on version {0}'.format(version.getId()))
  155.             try:
  156.                 components.extend(version.getComponents(location=location))
  157.  
  158.             # Possible to trigger if Component is uploaded to different location (ftrackreview)
  159.             except ftrack.ComponentNotInLocationError:
  160.                 self.logger.warning('# Not all Components were able to be found in: {0}'
  161.                                     .format(location))
  162.  
  163.                 # Only add components that are available in the specified location instance
  164.                 # Get all components attached to this Version, this time letting ftrack choose most
  165.                 # appropriate Location to use
  166.                 componentList = version.getComponents(location='auto')
  167.  
  168.                 # Now attempt to test if each Component exists in the specified Location
  169.                 # todo: handle components from different locations such as ftrack.review
  170.                 for component in componentList:
  171.  
  172.                     try:
  173.                         component.switchLocation(location)
  174.                         components.append(component)
  175.  
  176.                     except ftrack.ComponentNotInAnyLocationError:
  177.                         self.logger.warning('# {0} was skipped because it does not exist in any Location!'
  178.                             .format(component.getName()))
  179.  
  180.                     except ftrack.LocationError:
  181.                         self.logger.warning('# {0} was skipped because it does not exist in {1}!'
  182.                             .format(component.getName(), location.getName()))
  183.  
  184.         self.logger.info('Found {0} components in selection'.format(len(components)))
  185.         return components
  186.  
  187.  
  188.     @async
  189.     def transferComponents(
  190.         self, selection, sourceLocation, targetLocation,
  191.         userId=None,
  192.         ignoreComponentNotInLocation=False,
  193.         ignoreLocationErrors=False
  194.     ):
  195.         """
  196.        Transfer components in *selection* from *sourceLocation* to *targetLocation*.
  197.  
  198.        if *ignoreComponentNotInLocation*, ignore components missing in source
  199.        location. If *ignoreLocationErrors* is specified, ignore all locations-
  200.        related errors.
  201.  
  202.        Reports progress back to *userId* using a job.
  203.        """
  204.  
  205.         # Create ftrack API job visible in the UI
  206.         job = ftrack.createJob('Transfer components (Gathering...)', 'running', user=userId)
  207.  
  208.         try:
  209.             components = self.getComponentsInLocation(selection, sourceLocation)
  210.             amount = len(components)
  211.             self.logger.info('Transferring {0} components'.format(amount))
  212.  
  213.             # For each Component, transfer to target Location
  214.             for index, component in enumerate(components, start=1):
  215.  
  216.                 self.logger.info('Transferring component ({0} of {1})'.format(index, amount))
  217.                 job.setDescription('Transfer components ({0} of {1})'.format(index, amount))
  218.  
  219.                 assert isinstance(component, ftrack.Component), \
  220.                     '### {0} is not a Component object!'.format(component)
  221.  
  222.                 # get component id and resource identifier
  223.                 componentId = component.getId()
  224.                 componentResourceId = component.getResourceIdentifier()
  225.  
  226.                 try:
  227.                     # add the Component to the target location and switch in the API
  228.                     targetLocation.addComponent(component)
  229.                     component.switchLocation(targetLocation)
  230.  
  231.                 except ftrack.ComponentInLocationError:
  232.                     self.logger.warning('# Component: {0} already exists in target location!'
  233.                                      .format(component))
  234.  
  235.                     # Check if component does not exist in the filesystem
  236.                     accessor = targetLocation.getAccessor()
  237.  
  238.                     # Check for correct type of Accessor
  239.                     if isinstance(accessor, ftrack.DiskAccessor):
  240.  
  241.                         self.logger.info('Checking for resource on: {0}'.format(accessor))
  242.  
  243.                         if not accessor.exists(componentResourceId):
  244.                             self.logger.info('Resource: {0} does not exist on disk, re-transferring Component...'
  245.                                 .format(component.getName()))
  246.  
  247.                             # Do not manage data because the data does not exist, just delete from database
  248.                             targetLocation.removeComponent(componentId, manageData=False)
  249.  
  250.                             # now add component again
  251.                             targetLocation.addComponent(component)
  252.                             component.switchLocation(targetLocation)
  253.  
  254.                 except ftrack.ComponentNotInLocationError:
  255.                     if ignoreComponentNotInLocation or ignoreLocationErrors:
  256.                         self.logger.exception('Failed to add component to location')
  257.                     else:
  258.                         raise
  259.  
  260.                 except ftrack.LocationError:
  261.                     if ignoreLocationErrors:
  262.                         self.logger.exception('Failed to add component to location')
  263.                     else:
  264.                         raise
  265.  
  266.             job.setStatus('done')
  267.             self.logger.info('Transfer complete ({0} components)'.format(amount))
  268.  
  269.         except Exception:
  270.             self.logger.exception('Transfer failed!')
  271.             job.setStatus('failed')
  272.  
  273.  
  274.     def launch(self, event):
  275.         """
  276.        This method is invoked when the user executes the Action. This brings up
  277.        a UI to choose where to transfer the components to.
  278.        """
  279.  
  280.         selection = event['data'].get('selection', [])
  281.         userId = event['source']['user']['id']
  282.         self.logger.info(u'Launching action with selection: {0}'.format(selection))
  283.  
  284.         # Action has been executed
  285.         if 'values' in event['data']:
  286.             sourceLocation = ftrack.Location(event['data']['values']['from_location'])
  287.             targetLocation = ftrack.Location(event['data']['values']['to_location'])
  288.  
  289.             if sourceLocation == targetLocation:
  290.                 return {
  291.                     'success': False,
  292.                     'message': 'Source and target locations are the same!'
  293.                 }
  294.  
  295.             self.logger.info(event['data']['values'])
  296.  
  297.             # Get user preferences for component transfer options
  298.             ignoreComponentNotInLocation = (
  299.                 event['data']['values'].get('ignore_component_not_in_location') == 'true'
  300.             )
  301.             ignoreLocationErrors = (
  302.                 event['data']['values'].get('ignore_location_errors') == 'true'
  303.             )
  304.  
  305.             self.logger.info(
  306.                 'Transferring components from {0} to {1}'.format(sourceLocation, targetLocation)
  307.             )
  308.             self.transferComponents(
  309.                 selection,
  310.                 sourceLocation,
  311.                 targetLocation,
  312.                 userId=userId,
  313.                 ignoreComponentNotInLocation=ignoreComponentNotInLocation,
  314.                 ignoreLocationErrors=ignoreLocationErrors
  315.             )
  316.             return {
  317.                 'success': True,
  318.                 'message': 'Transferring components...'
  319.             }
  320.  
  321.         allLocations = [
  322.             {
  323.                 'label': location.get('name'),
  324.                 'value': location.get('id')
  325.             }
  326.             for location in ftrack.getLocations(excludeInaccessible=True)
  327.         ]
  328.  
  329.         if len(allLocations) < 2:
  330.             sourceLocation = ftrack.Location(event['data']['values']['from_location'])
  331.             targetLocation = ftrack.Location(event['data']['values']['to_location'])
  332.             if sourceLocation == targetLocation:
  333.                 return {
  334.                     'success': False,
  335.                     'message': 'Source and target locations are the same.'
  336.                 }
  337.             self.transferComponents(selection, sourceLocation, targetLocation)
  338.             return {
  339.                 'success': False,
  340.                 'message': 'Did not find two accessible locations'
  341.             }
  342.  
  343.         # Format dict to be used in the Custom Actions UI
  344.         return {
  345.             'items': [
  346.                 {
  347.                     'value': 'Transfer components between locations',
  348.                     'type': 'label'
  349.                 }, {
  350.                     'label': 'Source location',
  351.                     'type': 'enumerator',
  352.                     'name': 'from_location',
  353.                     'value': allLocations[0]['value'],
  354.                     'data': allLocations
  355.                 }, {
  356.                     'label': 'Target location',
  357.                     'type': 'enumerator',
  358.                     'name': 'to_location',
  359.                     'value': allLocations[1]['value'],
  360.                     'data': allLocations
  361.                 }, {
  362.                     'value': '---',
  363.                     'type': 'label'
  364.                 }, {
  365.                     'label': 'Ignore missing',
  366.                     'type': 'enumerator',
  367.                     'name': 'ignore_component_not_in_location',
  368.                     'value': 'false',
  369.                     'data': [
  370.                         {'label': 'Yes', 'value': 'true'},
  371.                         {'label': 'No', 'value': 'false'}
  372.                     ]
  373.                 }, {
  374.                     'label': 'Ignore errors',
  375.                     'type': 'enumerator',
  376.                     'name': 'ignore_location_errors',
  377.                     'value': 'false',
  378.                     'data': [
  379.                         {'label': 'Yes', 'value': 'true'},
  380.                         {'label': 'No', 'value': 'false'}
  381.                     ]
  382.                 }
  383.             ]
  384.         }
  385.  
  386.  
  387. def register(registry, **kwargs):
  388.     """
  389.    Register action. Called when used as an event plugin.
  390.    :param registry:
  391.    :param kwargs:
  392.    :return:
  393.    """
  394.  
  395.     # Validate plugin for legacy API to prevent double registration of plugin
  396.     if not isinstance(registry, ftrack.Registry):
  397.         return
  398.  
  399.     logger = logging.getLogger(__name__)
  400.  
  401.     logger.debug('Registering action: {0}'.format(
  402.         os.path.abspath(os.path.dirname(__file__))))
  403.  
  404.     action = TransferComponentsAction()
  405.     action.register()
  406.  
  407.  
  408. def main(arguments=None):
  409.     """
  410.    This entry point sets up logging and registers action when this is run
  411.    as a standalone action.
  412.    :param arguments:
  413.    :return:
  414.    """
  415.     if arguments is None:
  416.         arguments = []
  417.  
  418.     parser = argparse.ArgumentParser()
  419.     # Allow setting of logging level from arguments.
  420.     loggingLevels = {}
  421.     for level in (
  422.         logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
  423.         logging.ERROR, logging.CRITICAL
  424.     ):
  425.         loggingLevels[logging.getLevelName(level).lower()] = level
  426.  
  427.     parser.add_argument(
  428.         '-v', '--verbosity',
  429.         help='Set the logging output verbosity.',
  430.         choices=loggingLevels.keys(),
  431.         default='info'
  432.     )
  433.     namespace = parser.parse_args(arguments)
  434.  
  435.     # Set up basic logging
  436.     logging.basicConfig(level=loggingLevels[namespace.verbosity])
  437.  
  438.     # Subscribe to action.
  439.     ftrack.setup()
  440.     action = TransferComponentsAction()
  441.     action.register()
  442.  
  443.     # Wait for events
  444.     ftrack.EVENT_HUB.wait()
  445.  
  446.  
  447. if __name__ == '__main__':
  448.     raise SystemExit(main(sys.argv[1:]))
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement