Hooks

The Alias Engine defines hooks to customize the Toolkit Apps that it uses. See below for App specific hook details.

tk-multi-data-validation

AliasDataValidationHook

This hook will get the validation rule data from the AliasDataValidator and pass it to the tk-multi-data-validation App.

# Copyright (c) 2022 Autodesk, Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the ShotGrid Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the ShotGrid Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Autodesk, Inc.

from typing import Optional, List

import sgtk
import alias_api

HookBaseClass = sgtk.get_hook_baseclass()


class AliasDataValidationHook(HookBaseClass):
    """Hook to define Alias data validation functionality."""

    class AliasDataValidationError(Exception):
        """Custom exception class to report Alias Data Validation errors."""

    class SanitizedResult:
        """Class to represent a sanitized check result object."""

        def __init__(
            self,
            is_valid: Optional[bool] = None,
            errors: Optional[List] = None,
            error_count: int = 0,
        ):
            """
            Initialize the object with the given data.

            :param is_valid: The success status that the check function
                reported. If not provided, the validity will be determined
                based on if there are any errors.
            :param errors: The data errors the check function found. This
                should be a list of Alias objects, but can be a list ofobject
                as long as they follow the expected format.
            :param error_count: The number of errors found by the check
                function. This is useful when the errors list is not provided.
            """

            if is_valid is None:
                self.is_valid = not errors
            else:
                self.is_valid = is_valid

            if errors:
                error_list = []
                for item in errors:
                    if not item:
                        continue
                    if isinstance(item, str):
                        error_list.append(
                            {
                                "id": item,
                                "name": item,
                                "type": "",
                            }
                        )
                    elif isinstance(item, dict):
                        if "name" not in item:
                            raise self.AliasDataValidationError(
                                "Error dict missing 'name' key"
                            )
                        error_list.append(item)
                    else:
                        try:
                            error_list.append(
                                {
                                    "id": item.name or "",
                                    "name": item.name or "",
                                    "type": item.type(),
                                }
                            )
                        except AttributeError:
                            raise self.AliasDataValidationError(
                                f"Cannot sanitize error object {item}"
                            )
                self.errors = sorted(error_list, key=lambda x: x["name"].lower())
            else:
                self.errors = []

            self.error_count = len(errors) if errors else error_count

    def get_validation_data(self):
        """
        Return the validation rule data set to validate an Alias scene.

        This method will retrieve the default validation rules returned by
        :meth:`AliasDataValidator.get_validation_data`. To customize the default
        validation rules, override this hook method to modify the returned data dictionary.

        The dictionary returned by this function should be formated such that it can be passed
        to the :class:`~tk-multi-data-validation:api.data.ValidationRule` class constructor to
        create a new validation rule object.

        :return: The validation rules data set.
        :rtype: dict
        """

        data = self.parent.engine.data_validator.get_validation_data()

        # -------------------------------------------------------------------------------------------------------
        #
        # Example:
        #   How to add a custom rule to the default list of validation rules (the data above)
        #
        #   This example assume that the tk-multi-data-validation App is being used to display the validation
        #   data, and to perform the validation functionality.
        #
        # -------------------------------------------------------------------------------------------------------
        #
        #   Step (1) - define the necessary check, fix, and action callbacks that are required by your new rule.
        #   Step (2) - add your custom rule entry into the validation data dictionary `data`
        #   Step (3) - add your custom rule id to your tk-multi-data-validation.yml config settings
        #
        # -------------------------------------------------------------------------------------------------------
        #
        # # Define a global variable to toggle the custom rule valid state, for demonstrations purposes
        # global custom_rule_is_valid
        # custom_rule_is_valid = False
        #
        # def check_my_custom_rule(fail_fast=False):
        #     """
        #     This callback method will execute when the "Validate" button is clicked for this
        #     rule, or validate all is initiated.
        #
        #     NOTE that the check function takes one parameter, `fail_fast`, even if it is not
        #     used, it should still be defined to follow the "check"functions guidelines (see
        #     AliasDataValidator class for more info).
        #
        #     For examples of more advanced check functions, see the AliasDataValidator class
        #     methods prefixed with `check_`.
        #     """
        #
        #     if fail_fast:
        #         # In a fail fast context, just return True or False indicating if the rule is valid
        #         return custom_rule_is_valid
        #
        #     if custom_rule_is_valid:
        #         # Do not report any errors if the rule is valid
        #         errors = None
        #     else:
        #         # The rule is not valid, return the Alias objects that do not pass the check.
        #         # In this example, we using a namedtuple to return a list of Alias objects, but
        #         # a list of Alias objects retrieved from the Alias Python API can also be used
        #         # directly.
        #         from collections import namedtuple
        #         AliasObject = namedtuple("AliasObject", ["name", "type"])
        #         errors = [
        #             AliasObject("node#1", lambda: "AlSurfaceNode()"),
        #             AliasObject("node#2", lambda: "AlSurfaceNode()"),
        #         ]
        #
        #     # If not fail faist, check functions should return the list of error objects
        #     return errors
        #
        # def fix_my_custom_rule(errors=None):
        #     """
        #     This callback method will execute when the "Fix" button is clicked for this rule,
        #     or the fix all is initiated.
        #
        #     This fix function just sets the global valid state to True so that the next time
        #     the check function is executed, it will have a valid state of True.
        #
        #     NOTE that the fix function takes one parameter, `errors`, this is not used here
        #     for simplicity, but it should still be defined to follow the "fix" function
        #     guidelines (see AliasDataValidator class for more info).
        #
        #     For examples of more advanced fix functions, see the AliasDataValidator class
        #     methods prefixed with `fix_`.
        #     """
        #     global custom_rule_is_valid
        #     custom_rule_is_valid = True
        #
        # def action_callback(errors=None):
        #     """
        #     This callback method will execute when the action item is clicked for this rule.
        #
        #     The action item can be found by right-clicking the rule to see the menu actions,
        #     or by clicking the "..." on the rule item row. It is the action called
        #
        #         'Click me! Revalidate to see what happend >:)"
        #
        #     This action function just sets the global valid state to False so that the next time
        #     the check function is executed, it will have a valid state of False.
        #
        #     NOTE that the fix function takes one parameter, `errors`, this is not used here
        #     for simplicity, but it should still be defined to follow the action function
        #     guidelines (see AliasDataValidator class for more info).
        #
        #     For examples of more advanced fix functions, see the AliasDataValidator class
        #     methods.
        #     """
        #     global custom_rule_is_valid
        #     custom_rule_is_valid = False
        #
        # # Step (2)
        # data["my_custom_rule"] = {
        #     "name": "My Custom Validation Rule",
        #     "description": """
        #         This is an example to demonstrate how to add a custom rule. Try validating it ----------><br/>
        #         Right-click and 'Show Details' to open the right-hand panel to see more.
        #     """,
        #     "error_msg": "An error has been found by this rule. Let's fix it!",
        #     "fix_name": "Fix it!",
        #     "check_func": check_my_custom_rule,
        #     "fix_func": fix_my_custom_rule,
        #     "actions": [
        #         {"name": "Click me! Revalidate to see what happend >:)", "callback": action_callback}
        #     ]
        # }

        return data

    def sanitize_check_result(self, result):
        """
        Sanitize the value returned by any validate function to conform to the standard format.

        Convert the incoming list of Alias objects (that are errors) to conform to the standard
        format that the Data Validation App requires:

            is_valid:
                type: bool
                description: True if the validate function succeed with the current data, else
                             False.

            errors:
                type: list
                description: The list of error objects (found by the validate function). None
                             or empty list if the current data is valid.
                items:
                    type: dict
                    key-values:
                        id:
                            type: str | int
                            description: A unique identifier for the error object.
                            optional: False
                        name:
                            type: str
                            description: The display name for the error object.
                            optional: False
                        type:
                            type: str
                            description: The display name of the error object type.
                            optional: True

        This method will be called by the Data Validation App after any validate function is
        called, in order to receive the validate result in the required format.

        :param result: The result returned by a validation rule ``check_func``. This is
            should be a list of Alias objects or a boolean value indicating the validity of
            the check.
        :type result: list

        :return: The result of a ``check_func`` in the Data Validation standardized format.
        :rtype: dict
        """

        if isinstance(result, bool):
            return self.SanitizedResult(is_valid=result)

        if (
            isinstance(result, tuple)
            and len(result) == 2
            and isinstance(result[0], bool)
            and isinstance(result[1], int)
        ):
            return self.SanitizedResult(is_valid=result[0], error_count=result[1])

        if isinstance(result, list):
            return self.SanitizedResult(errors=result)

        raise self.AliasDataValidationError(
            "Cannot sanitize result type '{}'".format(type(result))
        )

    def post_fix_action(self, rules):
        """
        Called once an individual fix has been resolved

        :param rules: List of rule IDs linked to the executed fix
        """

        rule_ids = [rule.id for rule in rules]

        # force Alias to refresh its viewport for a specific set of fixes
        if not {"cos_unused", "references_exist", "node_pivots_at_origin"}.isdisjoint(
            rule_ids
        ):
            alias_api.redraw_screen()

tk-multi-loader2

AliasActions

# Copyright (c) 2015 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.

"""
Hook that loads defines all the available actions, broken down by publish type.
"""

import os

import sgtk

import alias_api


HookBaseClass = sgtk.get_hook_baseclass()


class AliasActions(HookBaseClass):
    def generate_actions(self, sg_publish_data, actions, ui_area):
        """
        Returns a list of action instances for a particular publish.
        This method is called each time a user clicks a publish somewhere in the UI.
        The data returned from this hook will be used to populate the actions menu for a publish.

        The mapping between Publish types and actions are kept in a different place
        (in the configuration) so at the point when this hook is called, the loader app
        has already established *which* actions are appropriate for this object.

        The hook should return at least one action for each item passed in via the
        actions parameter.

        This method needs to return detailed data for those actions, in the form of a list
        of dictionaries, each with name, params, caption and description keys.

        Because you are operating on a particular publish, you may tailor the output
        (caption, tooltip etc) to contain custom information suitable for this publish.

        The ui_area parameter is a string and indicates where the publish is to be shown.
        - If it will be shown in the main browsing area, "main" is passed.
        - If it will be shown in the details area, "details" is passed.
        - If it will be shown in the history area, "history" is passed.

        Please note that it is perfectly possible to create more than one action "instance" for
        an action! You can for example do scene introspection - if the action passed in
        is "character_attachment" you may for example scan the scene, figure out all the nodes
        where this object can be attached and return a list of action instances:
        "attach to left hand", "attach to right hand" etc. In this case, when more than
        one object is returned for an action, use the params key to pass additional
        data into the run_action hook.

        :param sg_publish_data: Flow Production Tracking data dictionary with all the standard publish fields.
        :param actions: List of action strings which have been defined in the app configuration.
        :param ui_area: String denoting the UI Area (see above).
        :returns List of dictionaries, each with keys name, params, caption and description
        """

        self.logger.debug(
            "Generate actions called for UI element %s. "
            "Actions: %s. Publish Data: %s" % (ui_area, actions, sg_publish_data)
        )

        action_instances = []

        if "reference" in actions:
            action_instances.append(
                {
                    "name": "reference",
                    "params": None,
                    "caption": "Create Reference",
                    "description": "This will add the item to the universe as a standard reference.",
                }
            )

        if "import" in actions:
            action_instances.append(
                {
                    "name": "import",
                    "params": None,
                    "caption": "Import into Scene",
                    "description": "This will import the item into the current universe.",
                }
            )

        if "import_as_reference" in actions:
            action_instances.append(
                {
                    "name": "import_as_reference",
                    "params": None,
                    "caption": "Import as Reference",
                    "description": "This will import the item as a reference into the current universe.",
                }
            )

        if "texture_node" in actions:
            action_instances.append(
                {
                    "name": "texture_node",
                    "params": None,
                    "caption": "Create Canvas",
                    "description": "This will import the item into the current universe.",
                }
            )

        if "import_subdiv" in actions:
            action_instances.append(
                {
                    "name": "import_subdiv",
                    "params": None,
                    "caption": "Import Subdiv file into Scene",
                    "description": "This will import the subdiv item into the current universe.",
                }
            )

        return action_instances

    def execute_multiple_actions(self, actions):
        """
        Executes the specified action on a list of items.

        The default implementation dispatches each item from ``actions`` to
        the ``execute_action`` method.

        The ``actions`` is a list of dictionaries holding all the actions to execute.
        Each entry will have the following values:

            name: Name of the action to execute
            sg_publish_data: Publish information coming from Flow Production Tracking
            params: Parameters passed down from the generate_actions hook.

        .. note::
            This is the default entry point for the hook. It reuses the ``execute_action``
            method for backward compatibility with hooks written for the previous
            version of the loader.

        .. note::
            The hook will stop applying the actions on the selection if an error
            is raised midway through.

        :param list actions: Action dictionaries.
        """
        for single_action in actions:
            name = single_action["name"]
            sg_publish_data = single_action["sg_publish_data"]
            params = single_action["params"]
            self.execute_action(name, params, sg_publish_data)

    def execute_action(self, name, params, sg_publish_data):
        """
        Execute a given action. The data sent to this be method will
        represent one of the actions enumerated by the generate_actions method.

        :param name: Action name string representing one of the items returned by generate_actions.
        :param params: Params data, as specified by generate_actions.
        :param sg_publish_data: Flow Production Tracking data dictionary with all the standard publish fields.
        :returns: No return value expected.
        """

        self.logger.debug(
            "Execute action called for action %s. "
            "Parameters: %s. Publish Data: %s" % (name, params, sg_publish_data)
        )

        # resolve path
        path = self.get_publish_path(sg_publish_data)

        if name == "reference":
            self._create_reference(path)

        elif name == "import":
            self._import_file(path)

        elif name == "import_as_reference":
            self._import_file_as_reference(path, sg_publish_data)

        elif name == "texture_node":
            self._create_texture_node(path)

        elif name == "import_subdiv":
            self._import_subdivision(path)

    def _create_reference(self, path):
        """
        Create an Alias reference.

        :param path: Path to the file.
        """
        if not os.path.exists(path):
            raise Exception("File not found on disk - '%s'" % path)
        alias_api.create_reference(path)

    def _import_file(self, path):
        """
        Import the file into the current Alias session.

        :param path: Path to file.
        """
        if not os.path.exists(path):
            raise Exception("File not found on disk - '%s'" % path)
        alias_api.import_file(path)

    def _import_file_as_reference(self, path, sg_publish_data):
        """
        Import the file as an Alias reference, converting it on the fly as wref.

        :param path:            Path to the file.
        :param sg_publish_data: Flow Production Tracking data dictionary with all the standard publish fields
        """

        # get the tank of the project the file we're trying to import belongs to
        # this will be useful to manipulate configuration settings and templates
        tk = self.parent.engine.get_tk_from_project(sg_publish_data["project"])

        # then, get the reference template and the source template to be able to extract fields and build the path to
        # the translated file
        try:
            reference_template = self.parent.engine.get_reference_template(
                tk, sg_publish_data
            )
        except AttributeError:
            raise Exception(
                "There is an issue with Pipeline Configurations for the Linked Project<br>"
                " Please see the guidelines <a href=https://github.com/shotgunsoftware/tk-alias/wiki/Loading#import-as-reference-from-another-project><b>here.</b></a>"
            )
            return

        source_template = tk.template_from_path(path)

        # get the path to the reference, using the templates if it's possible otherwise using the source path
        # location
        if reference_template and source_template:
            template_fields = source_template.get_fields(path)
            template_fields["alias.extension"] = os.path.splitext(path)[-1][1:]
            reference_path = reference_template.apply_fields(template_fields)
        else:
            output_path, output_ext = os.path.splitext(path)
            reference_path = "{output_path}_{output_ext}.wref".format(
                output_path=output_path, output_ext=output_ext[1:]
            )

        # if the reference file doesn't exist on disk yet, run the translation
        if not os.path.exists(reference_path):

            framework = self.load_framework("tk-framework-aliastranslations_v0.x.x")
            if not framework:
                raise Exception("Couldn't find tk-framework-aliastranslations_v0.x.x")
            tk_framework_aliastranslations = framework.import_module(
                "tk_framework_aliastranslations"
            )

            translator = tk_framework_aliastranslations.Translator(path, reference_path)
            translator.execute()

        alias_api.create_reference(reference_path)

    def _create_texture_node(self, path):
        """
        Import an image as Canvas in Alias

        :param path:  Path to the image.
        """
        if not os.path.exists(path):
            raise Exception("File not found on disk - '%s'" % path)
        alias_api.create_texture_node(path, True)

    def _import_subdivision(self, path):
        """
        Import a file as subdivision in the current Alias session.

        :param path: Path to the file.
        """
        if not os.path.exists(path):
            raise Exception("File not found on disk - '%s'" % path)

        try:
            alias_api.import_subdiv(path)
        except alias_api.AliasPythonException as api_error:
            err_msg = "{api_error}<br/><br/>For more information, click {help_link}.".format(
                api_error=str(api_error),
                help_link="<a href='https://help.autodesk.com/view/ALIAS/2023/ENU/?guid=GUID-667410AD-CF4D-43B3-AE96-0C1331CB80B2'>here</a>",
            )
            raise alias_api.AliasPythonException(err_msg)

tk-multi-publish2

PublishAnnotationsPlugin

# Copyright (c) 2017 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.

import sgtk
import alias_api

HookBaseClass = sgtk.get_hook_baseclass()


class PublishAnnotationsPlugin(HookBaseClass):
    """
    Plugin for publishing annotations of the current alias open session
    """

    @property
    def name(self):
        """
        One line display name describing the plugin
        """
        return "Publish Annotations to Flow Production Tracking"

    @property
    def description(self):
        return """
        <p>
            This plugin exports all annotations created using the Locator Annotation tool in Alias.
        </p>
        <p>
            Each annotation will create a Note in Flow Production Tracking. All Notes are linked to this version and file. Use this to
            sync all review notes made in Alias with Flow Production Tracking.
        </p>
        """

    @property
    def item_filters(self):
        """
        List of item types that this plugin is interested in.

        Only items matching entries in this list will be presented to the
        accept() method. Strings can contain glob patters such as *, for example
        ["maya.*", "file.maya"]
        """
        return ["alias.session"]

    def accept(self, settings, item):
        """
        Method called by the publisher to determine if an item is of any
        interest to this plugin. Only items matching the filters defined via the
        item_filters property will be presented to this method.

        A publish task will be generated for each item accepted here. Returns a
        dictionary with the following booleans:

            - accepted: Indicates if the plugin is interested in this value at
                all. Required.
            - enabled: If True, the plugin will be enabled in the UI, otherwise
                it will be disabled. Optional, True by default.
            - visible: If True, the plugin will be visible in the UI, otherwise
                it will be hidden. Optional, True by default.
            - checked: If True, the plugin will be checked in the UI, otherwise
                it will be unchecked. Optional, True by default.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process

        :returns: dictionary with boolean keys accepted, required and enabled
        """

        if not alias_api.has_annotation_locator():
            self.logger.debug("There are not annotations to export")
            return {"accepted": False}

        return {"accepted": True, "checked": False}

    def validate(self, settings, item):
        """
        Validates the given item to check that it is ok to publish. Returns a
        boolean to indicate validity.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        :returns: True if item is valid, False otherwise.
        """

        return True

    def publish(self, settings, item):
        """
        Executes the publish logic for the given item and settings.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        """

        # get the publish "mode" stored inside of the root item properties
        bg_processing = item.parent.properties.get("bg_processing", False)
        in_bg_process = item.parent.properties.get("in_bg_process", False)

        if not bg_processing or (bg_processing and in_bg_process):

            self.logger.info("Publishing annotations")

            # Links, the note will be attached to published file by default
            # if a version is created the note will be attached to this too
            publish_data = item.properties["sg_publish_data"]
            version_data = item.properties.get("sg_version_data")

            note_links = [publish_data]
            if version_data is not None:
                note_links.append(version_data)

            annotations = alias_api.get_annotation_locator_strings()

            batch_data = []
            for annotation in annotations:
                note_data = {
                    "project": item.context.project,
                    "user": item.context.user,
                    "subject": "Alias Annotation",
                    "content": annotation,
                    "note_links": note_links,
                }
                if item.context.task:
                    note_data["tasks"] = [item.context.task]
                batch_data.append(
                    {"request_type": "create", "entity_type": "Note", "data": note_data}
                )

            if batch_data:
                self.parent.shotgun.batch(batch_data)

    def finalize(self, settings, item):
        """
        Execute the finalization pass. This pass executes once all the publish
        tasks have completed, and can for example be used to version up files.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        """

        # get the publish "mode" stored inside of the root item properties
        bg_processing = item.parent.properties.get("bg_processing", False)
        in_bg_process = item.parent.properties.get("in_bg_process", False)

        if not bg_processing or (bg_processing and in_bg_process):
            self.logger.info("Annotations published successfully")

AliasSessionPublishPlugin

# Copyright (c) 2017 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.

import os
import sgtk
import alias_api

HookBaseClass = sgtk.get_hook_baseclass()


class AliasSessionPublishPlugin(HookBaseClass):
    """
    Plugin for publishing an open alias session.

    This hook relies on functionality found in the base file publisher hook in
    the publish2 app and should inherit from it in the configuration. The hook
    setting for this plugin should look something like this::

        hook: "{self}/publish_file.py:{engine}/tk-multi-publish2/basic/publish_session.py"

    """

    # Publish mode string constants
    PUBLISH_MODE_DEFAULT = "Default"
    PUBLISH_MODE_EXPORT_SELECTION = "Export Selection"

    # Publish mode options
    PUBLISH_MODE_OPTIONS = [
        PUBLISH_MODE_DEFAULT,
        PUBLISH_MODE_EXPORT_SELECTION,
    ]

    @property
    def description(self):
        """
        Verbose, multi-line description of what the plugin does. This can
        contain simple html for formatting.
        """

        loader_url = "https://help.autodesk.com/view/SGDEV/ENU/?contextId=PC_APP_LOADER"

        return """
        Publishes the file to Flow Production Tracking. A <b>Publish</b>
        entry will be created in Flow Production Tracking which will
        include a reference to the file's current path on disk. If a
        publish template is configured, a copy of the current session
        will be copied to the publish template path which will be the
        file that is published. Other users will be able to access the
        published file via the <b><a href='%s'>Loader</a></b> so long as
        they have access to the file's location on disk.

        If the session has not been saved, validation will fail and a button
        will be provided in the logging output to save the file.

        <h3>File versioning</h3>
        If the filename contains a version number, the process will bump the
        file to the next version after publishing.

        The <code>version</code> field of the resulting <b>Publish</b> in
        Flow Production Tracking will also reflect the version number identified
        in the filename. The basic worklfow recognizes the following version
        formats by default:

        <ul>
        <li><code>filename.v###.ext</code></li>
        <li><code>filename_v###.ext</code></li>
        <li><code>filename-v###.ext</code></li>
        </ul>

        After publishing, if a version number is detected in the work file, the
        work file will automatically be saved to the next incremental version
        number. For example, <code>filename.v001.ext</code> will be published
        and copied to <code>filename.v002.ext</code>

        If the next incremental version of the file already exists on disk, the
        validation step will produce a warning, and a button will be provided in
        the logging output which will allow saving the session to the next
        available version number prior to publishing.

        <br><br><i>NOTE: any amount of version number padding is supported. for
        non-template based workflows.</i>

        <h3>Overwriting an existing publish</h3>
        In non-template workflows, a file can be published multiple times,
        however only the most recent publish will be available to other users.
        Warnings will be provided during validation if there are previous
        publishes.
        """ % (
            loader_url,
        )

    @property
    def settings(self):
        """
        Dictionary defining the settings that this plugin expects to receive
        through the settings parameter in the accept, validate, publish and
        finalize methods.

        A dictionary on the following form::

            {
                "Settings Name": {
                    "type": "settings_type",
                    "default": "default_value",
                    "description": "One line description of the setting"
            }

        The type string should be one of the data types that toolkit accepts as
        part of its environment configuration.
        """

        # inherit the settings from the base publish plugin
        base_settings = super(AliasSessionPublishPlugin, self).settings or {}

        # settings specific to this class
        alias_publish_settings = {
            "Publish Template": {
                "type": "template",
                "default": None,
                "description": "Template path for published work files. Should"
                "correspond to a template defined in "
                "templates.yml.",
            },
            "Publish Mode": {
                "type": "str",
                "default": self.PUBLISH_MODE_DEFAULT,
                "description": "The mode to use when publishing the session. User can choose between 'Default' and 'Export Selection'.",
            },
        }

        # update the base settings
        base_settings.update(alias_publish_settings)

        return base_settings

    @property
    def item_filters(self):
        """
        List of item types that this plugin is interested in.

        Only items matching entries in this list will be presented to the
        accept() method. Strings can contain glob patters such as *, for example
        ["maya.*", "file.maya"]
        """
        return ["alias.session"]

    def accept(self, settings, item):
        """
        Method called by the publisher to determine if an item is of any
        interest to this plugin. Only items matching the filters defined via the
        item_filters property will be presented to this method.

        A publish task will be generated for each item accepted here. Returns a
        dictionary with the following booleans:

            - accepted: Indicates if the plugin is interested in this value at
                all. Required.
            - enabled: If True, the plugin will be enabled in the UI, otherwise
                it will be disabled. Optional, True by default.
            - visible: If True, the plugin will be visible in the UI, otherwise
                it will be hidden. Optional, True by default.
            - checked: If True, the plugin will be checked in the UI, otherwise
                it will be unchecked. Optional, True by default.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process

        :returns: dictionary with boolean keys accepted, required and enabled
        """

        # if a publish template is configured, disable context change. This
        # is a temporary measure until the publisher handles context switching
        # natively.
        if settings.get("Publish Template").value:
            item.context_change_allowed = False

        path = _session_path()

        if not path:
            # the session has not been saved before (no path determined).
            # provide a save button. the session will need to be saved before
            # validation will succeed.
            self.logger.warning(
                "The Alias session has not been saved.",
                extra=_get_save_as_action(),
            )

        self.logger.info(
            "Alias '%s' plugin accepted the current Alias session." % (self.name,)
        )
        return {"accepted": True, "checked": True}

    def validate(self, settings, item):
        """
        Validates the given item to check that it is ok to publish. Returns a
        boolean to indicate validity.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        :returns: True if item is valid, False otherwise.
        """

        publisher = self.parent
        path = _session_path()

        # ---- ensure the valid publish mode
        publish_mode = settings.get("Publish Mode").value
        if publish_mode not in self.PUBLISH_MODE_OPTIONS:
            self.logger.error(f"Unsupported Publish Mode {publish_mode}")
            return False

        if publish_mode == self.PUBLISH_MODE_EXPORT_SELECTION:
            bg_processing = item.parent.get_property("bg_processing", False)
            if bg_processing:
                error_msg = "Export Selection mode is not supported with Background Publishing. Please change the Publish Mode or turn off Background Publishing."
                self.logger.error(error_msg)
                return False

        # ---- ensure the session has been saved

        if not path:
            # the session still requires saving. provide a save button.
            # validation fails.
            error_msg = "The Alias session has not been saved."
            self.logger.error(
                error_msg,
                extra=_get_save_as_action(),
            )
            raise Exception(error_msg)

        # ---- check that references exist, display warning for invalid refs

        for reference in alias_api.get_references():
            ref_path = reference.path
            if not os.path.exists(ref_path):
                self.logger.warning(
                    "Reference path does not exist '{}'".format(ref_path)
                )

        # ---- check the session against any attached work template

        # get the path in a normalized state. no trailing separator,
        # separators are appropriate for current os, no double separators,
        # etc.
        path = sgtk.util.ShotgunPath.normalize(path)

        # if the session item has a known work template, see if the path
        # matches. if not, warn the user and provide a way to save the file to
        # a different path
        work_template = item.properties.get("work_template")
        if work_template:
            if not work_template.validate(path):
                error_msg = "The current session does not match the configured work file template."
                self.logger.warning(
                    error_msg,
                    extra={
                        "action_button": {
                            "label": "Save File",
                            "tooltip": "Save the current Alias session to a "
                            "different file name",
                            "callback": sgtk.platform.current_engine().open_save_as_dialog,
                        }
                    },
                )
                raise Exception(error_msg)
            else:
                self.logger.debug("Work template configured and matches session file.")
        else:
            self.logger.debug("No work template configured.")

        # ---- see if the version can be bumped post-publish

        # check to see if the next version of the work file already exists on
        # disk. if so, warn the user and provide the ability to jump to save
        # to that version now
        (next_version_path, version) = self._get_next_version_info(path, item)
        if next_version_path and os.path.exists(next_version_path):

            # determine the next available version_number. just keep asking for
            # the next one until we get one that doesn't exist.
            while os.path.exists(next_version_path):
                (next_version_path, version) = self._get_next_version_info(
                    next_version_path, item
                )

            error_msg = "The next version of this file already exists on disk."
            self.logger.error(
                error_msg,
                extra={
                    "action_button": {
                        "label": "Save to v%s" % (version,),
                        "tooltip": "Save to the next available version number, "
                        "v%s" % (version,),
                        "callback": lambda: publisher.engine.save_file_as(
                            next_version_path
                        ),
                    }
                },
            )
            raise Exception(error_msg)

        # ---- populate the necessary properties and call base class validation

        # populate the publish template on the item if found
        publish_template_setting = settings.get("Publish Template")
        publish_template = publisher.engine.get_template_by_name(
            publish_template_setting.value
        )
        if publish_template:
            item.properties["publish_template"] = publish_template

        # set the session path on the item for use by the base plugin validation
        # step. NOTE: this path could change prior to the publish phase.
        item.properties["path"] = path

        # run the base class validation
        return super(AliasSessionPublishPlugin, self).validate(settings, item)

    def publish(self, settings, item):
        """
        Executes the publish logic for the given item and settings.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        """

        # get the publish "mode" stored inside of the root item properties
        bg_processing = item.parent.properties.get("bg_processing", False)
        in_bg_process = item.parent.properties.get("in_bg_process", False)

        # get the path in a normalized state. no trailing separator, separators
        # are appropriate for current os, no double separators, etc.
        path = sgtk.util.ShotgunPath.normalize(_session_path())

        # ensure the session is saved
        # we need to do this action locally to be sure the background process could access the work file
        if not bg_processing or (bg_processing and not in_bg_process):
            # Save the working file before publishing
            self.parent.engine.save_file()

            # store the current session path in the root item properties
            # it will be used later in the background process to open the file before running the publishing actions
            if bg_processing and "session_path" not in item.parent.properties:
                item.parent.properties["session_path"] = path
                item.parent.properties[
                    "session_name"
                ] = "Alias Session - {task_name}, {entity_type} {entity_name} - {file_name}".format(
                    task_name=item.context.task["name"],
                    entity_type=item.context.entity["type"],
                    entity_name=item.context.entity["name"],
                    file_name=os.path.basename(path),
                )

        # update the item with the saved session path
        item.properties["path"] = path

        if not bg_processing or (bg_processing and in_bg_process):

            # add dependencies for the base class to register when publishing
            item.properties[
                "publish_dependencies"
            ] = _alias_find_additional_session_dependencies()

            # let the base class register the publish
            super(AliasSessionPublishPlugin, self).publish(settings, item)

    def finalize(self, settings, item):
        """
        Execute the finalization pass. This pass executes once all the publish
        tasks have completed, and can for example be used to version up files.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        """

        # get the publish "mode" stored inside of the root item properties
        bg_processing = item.parent.properties.get("bg_processing", False)
        in_bg_process = item.parent.properties.get("in_bg_process", False)

        if not bg_processing or (bg_processing and in_bg_process):
            # do the base class finalization
            super(AliasSessionPublishPlugin, self).finalize(settings, item)

        # bump the session file to the next version
        if not bg_processing or (bg_processing and not in_bg_process):
            self._save_to_next_version(
                item.properties["path"], item, self.parent.engine.save_file_as
            )

    ############################################################################
    # Methods for creating/displaying custom plugin interface

    def create_settings_widget(self, parent, items=None):
        """
        Creates a Qt widget, for the supplied parent widget (a container widget
        on the right side of the publish UI).

        :param parent: The parent to use for the widget being created.
        :param items: A list of PublishItems the selected publish tasks are parented to.
        :return: A QtGui.QWidget or subclass that displays information about
            the plugin and/or editable widgets for modifying the plugin's
            settings.
        """

        # defer Qt-related imports
        from sgtk.platform.qt import QtGui

        # The main widget
        widget = QtGui.QWidget(parent)
        widget_layout = QtGui.QVBoxLayout()

        # The description widget
        description_groupbox = super().create_settings_widget(parent, items)

        # Add a combobox to edit the publish mode
        publish_mode_label = QtGui.QLabel("Publish Mode:")
        publish_mode_combobox = QtGui.QComboBox(widget)
        publish_mode_combobox.setAccessibleName("Publish mode selection dropdown")
        publish_mode_combobox.addItems(self.PUBLISH_MODE_OPTIONS)
        publish_mode_widget = QtGui.QWidget(widget)
        publish_mode_widget.setSizePolicy(
            QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred
        )
        publish_mode_layout = QtGui.QHBoxLayout()
        publish_mode_layout.setContentsMargins(0, 0, 0, 0)
        publish_mode_layout.addWidget(publish_mode_label)
        publish_mode_layout.addWidget(publish_mode_combobox)
        publish_mode_layout.addStretch()
        publish_mode_widget.setLayout(publish_mode_layout)

        # Add all the minor widgets to the main widget
        widget_layout.addWidget(publish_mode_widget)
        widget_layout.addWidget(description_groupbox)
        widget.setLayout(widget_layout)

        # Set the widget property to store the combobox to access in get_ui_settings and set_ui_settings
        widget.setProperty("publish_mode_combobox", publish_mode_combobox)

        return widget

    def get_ui_settings(self, widget, items=None):
        """
        This method is required to be defined in order for the custom UI to show up in the app.

        Invoked by the Publisher when the selection changes. This method gathers the settings
        on the previously selected task, so that they can be later used to repopulate the
        custom UI if the task gets selected again. They will also be passed to the accept, validate,
        publish and finalize methods, so that the settings can be used to drive the publish process.

        The widget argument is the widget that was previously created by
        `create_settings_widget`.

        The method returns a dictionary, where the key is the name of a
        setting that should be updated and the value is the new value of that
        setting. Note that it is up to you how you want to store the UI's state as
        settings and you don't have to necessarily to return all the values from
        the UI. This is to allow the publisher to update a subset of settings
        when multiple tasks have been selected.

        Example::

            {
                 "setting_a": "/path/to/a/file"
            }

        :param widget: The widget that was created by `create_settings_widget`
        """

        ui_settings = {}

        # Get the Publish Mode settings value from the UI combobox
        publish_mode_combobox = widget.property("publish_mode_combobox")
        if publish_mode_combobox:
            mode_index = publish_mode_combobox.currentIndex()
            if 0 <= mode_index < len(self.PUBLISH_MODE_OPTIONS):
                ui_settings["Publish Mode"] = self.PUBLISH_MODE_OPTIONS[mode_index]
            else:
                self.logger.debug(f"Invalid Publish Mode index {mode_index}")

        return ui_settings

    def set_ui_settings(self, widget, settings, items=None):
        """
        This method is required to be defined in order for the custom UI to show up in the app.

        Allows the custom UI to populate its fields with the settings from the
        currently selected tasks.

        The widget is the widget created and returned by
        `create_settings_widget`.

        A list of settings dictionaries are supplied representing the current
        values of the settings for selected tasks. The settings dictionaries
        correspond to the dictionaries returned by the settings property of the
        hook.

        Example::

            settings = [
            {
                 "seeting_a": "/path/to/a/file"
                 "setting_b": False
            },
            {
                 "setting_a": "/path/to/a/file"
                 "setting_b": False
            }]

        The default values for the settings will be the ones specified in the
        environment file. Each task has its own copy of the settings.

        When invoked with multiple settings dictionaries, it is the
        responsibility of the custom UI to decide how to display the
        information. If you do not wish to implement the editing of multiple
        tasks at the same time, you can raise a ``NotImplementedError`` when
        there is more than one item in the list and the publisher will inform
        the user than only one task of that type can be edited at a time.

        :param widget: The widget that was created by `create_settings_widget`.
        :param settings: a list of dictionaries of settings for each selected
            task.
        :param items: A list of PublishItems the selected publish tasks are parented to.
        """

        if not settings:
            return

        if len(settings) > 1:
            raise NotImplementedError

        publish_mode_combobox = widget.property("publish_mode_combobox")
        if not publish_mode_combobox:
            self.logger.debug(
                "Failed to retrieve Publish Mode combobox to set custom UI"
            )
            return

        # Get the default setting for publish mode
        default_value = self.settings.get("Publish Mode", {}).get("default")

        # Get the publish mode value from the settings, and set the combobox accordingly
        publish_mode = settings[0].get("Publish Mode", default_value)
        try:
            publish_mode_index = max(self.PUBLISH_MODE_OPTIONS.index(publish_mode), 0)
        except ValueError:
            publish_mode_index = 0

        # Set the publish mode combobox
        current_version_index = publish_mode_combobox.currentIndex()
        if current_version_index == publish_mode_index:
            return  # Nothing to do

        publish_mode_combobox.setCurrentIndex(publish_mode_index)

    ############################################################################
    # protected methods

    def _copy_to_publish(self, settings, item):
        """
        Copy the item file to the publish location.

        :param settings: This plugin instance's configured settings.
        :param item: The item containing the file to copy.
        """

        publish_mode = settings.get("Publish Mode").value
        if publish_mode == self.PUBLISH_MODE_EXPORT_SELECTION:
            publish_path = self.get_publish_path(settings, item)
            self.parent.engine.alias_py.store_active(publish_path)
        else:
            super()._copy_to_publish(settings, item)


def _alias_find_additional_session_dependencies():
    """
    Find additional dependencies from the session
    """

    references = []
    for reference in alias_api.get_references():
        path = reference.path
        if path not in references and os.path.exists(path):
            references.append(path)

    return references


def _session_path():
    """
    Return the path to the current session
    :return:
    """

    return alias_api.get_current_path()


def _get_save_as_action():
    """Simple helper for returning a log action to show the "File Save As" dialog"""
    return {
        "action_button": {
            "label": "Save As...",
            "tooltip": "Save the current session",
            "callback": sgtk.platform.current_engine().open_save_as_dialog,
        }
    }

AliasTranslationPublishPlugin

# Copyright (c) 2017 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.

import os
import sgtk
import time

import alias_api

HookBaseClass = sgtk.get_hook_baseclass()


class AliasTranslationPublishPlugin(HookBaseClass):
    """
    Plugin for publishing an open alias session.

    This hook relies on functionality found in the base file publisher hook in
    the publish2 app and should inherit from it in the configuration. The hook
    setting for this plugin should look something like this::

        hook: "{self}/publish_file.py:{engine}/tk-multi-publish2/basic/publish_session.py"

    """

    # NOTE: The plugin icon and name are defined by the base file plugin.

    @property
    def description(self):
        """
        Verbose, multi-line description of what the plugin does. This can
        contain simple html for formatting.
        """

        loader_url = "https://help.autodesk.com/view/SGDEV/ENU/?contextId=PC_APP_LOADER"

        return """
        Publishes the file to Flow Production Tracking. A <b>Publish</b>
        entry will be created in Flow Production Tracking which will include
        a reference to the file's current path on disk. If a publish template
        is configured, a copy of the current session will be copied to the
        publish template path which will be the file that is published. Other
        users will be able to access the published file via the <b>
        <a href='%s'>Loader</a></b> so long as they have access to the file's
        location on disk.

        <br><br><b color='red'>NOTE:</b> it's not possible to publish a WREF file
        if you already have WREF files loaded in your current session.
        """ % (
            loader_url,
        )

    @property
    def settings(self):
        """
        Dictionary defining the settings that this plugin expects to receive
        through the settings parameter in the accept, validate, publish and
        finalize methods.

        A dictionary on the following form::

            {
                "Settings Name": {
                    "type": "settings_type",
                    "default": "default_value",
                    "description": "One line description of the setting"
            }

        The type string should be one of the data types that toolkit accepts as
        part of its environment configuration.
        """
        # inherit the settings from the base publish plugin
        base_settings = super(AliasTranslationPublishPlugin, self).settings or {}

        # settings specific to this class
        alias_publish_settings = {
            "Publish Template": {
                "type": "template",
                "default": None,
                "description": "Template path for published work files. Should"
                "correspond to a template defined in "
                "templates.yml.",
            }
        }

        # update the base settings
        base_settings.update(alias_publish_settings)

        # translator settings
        translator_settings = {
            "Translator Settings": {
                "type": "list",
                "default": [],
                "description": "Translator settings used to set values like file release number for CATPArt, among "
                "others. To see all the available options per format, you can look at the command line "
                "parameters",
            }
        }

        # update the base settings
        base_settings.update(translator_settings)

        return base_settings

    @property
    def item_filters(self):
        """
        List of item types that this plugin is interested in.

        Only items matching entries in this list will be presented to the
        accept() method. Strings can contain glob patters such as *, for example
        ["maya.*", "file.maya"]
        """
        return ["alias.session.translation"]

    def accept(self, settings, item):
        """
        Method called by the publisher to determine if an item is of any
        interest to this plugin. Only items matching the filters defined via the
        item_filters property will be presented to this method.

        A publish task will be generated for each item accepted here. Returns a
        dictionary with the following booleans:

            - accepted: Indicates if the plugin is interested in this value at
                all. Required.
            - enabled: If True, the plugin will be enabled in the UI, otherwise
                it will be disabled. Optional, True by default.
            - visible: If True, the plugin will be visible in the UI, otherwise
                it will be hidden. Optional, True by default.
            - checked: If True, the plugin will be checked in the UI, otherwise
                it will be unchecked. Optional, True by default.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process

        :returns: dictionary with boolean keys accepted, required and enabled
        """

        publisher = self.parent

        publish_template_setting = settings.get("Publish Template").value
        if publish_template_setting:

            # if a publish template is configured, disable context change. This
            # is a temporary measure until the publisher handles context switching
            # natively.
            item.context_change_allowed = False

            # get the publish template definition to determine if we are trying to publish a WREF file.
            # If so, disable the plugin if some references are loaded in the current session
            publish_template = publisher.engine.get_template_by_name(
                publish_template_setting
            )
            if publish_template and "wref" in publish_template.definition:
                alias_references = alias_api.get_references()
                if alias_references:
                    return {"accepted": True, "enabled": False, "checked": False}

        path = _session_path()

        if not path:
            # the session has not been saved before (no path determined).
            # provide a save button. the session will need to be saved before
            # validation will succeed.
            self.logger.warn(
                "The Alias session has not been saved.",
                extra=_get_save_as_action(),
            )

        self.logger.info(
            "Alias '%s' plugin accepted the current Alias session." % (self.name,)
        )

        return {"accepted": True, "checked": False}

    def validate(self, settings, item):
        """
        Validates the given item to check that it is ok to publish. Returns a
        boolean to indicate validity.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        :returns: True if item is valid, False otherwise.
        """

        publisher = self.parent

        path = _session_path()

        # ---- ensure the session has been saved

        if not path:
            # the session still requires saving. provide a save button.
            # validation fails.
            error_msg = "The Alias session has not been saved."
            self.logger.error(
                error_msg,
                extra=_get_save_as_action(),
            )
            raise Exception(error_msg)

        # ---- check the session against any attached work template

        # get the path in a normalized state. no trailing separator,
        # separators are appropriate for current os, no double separators,
        # etc.
        path = sgtk.util.ShotgunPath.normalize(path)

        # if the session item has a known work template, see if the path
        # matches. if not, warn the user and provide a way to save the file to
        # a different path
        work_template = item.properties.get("work_template")
        if not work_template or not work_template.validate(path):
            self.logger.warning(
                "The current session does not match the configured work "
                "file template.",
                extra={
                    "action_button": {
                        "label": "Save File",
                        "tooltip": "Save the current VRED session to a "
                        "different file name",
                        "callback": sgtk.platform.current_engine().open_save_as_dialog,
                    }
                },
            )
            return False
        else:
            self.logger.debug("Work template configured and matches session file.")

        # ---- populate the necessary properties and call base class validation

        # set the session path on the item for use by the base plugin validation
        # step. NOTE: this path could change prior to the publish phase.
        item.properties["path"] = path

        # if we don't have a publish path, we can't publish
        publish_path = self.get_publish_path(settings, item)
        if not publish_path:
            self.logger.warning(
                "Couldn't find a valid publish path for the translation file."
            )
            return False

        # use the framework to get the Alias translator
        framework = self.load_framework("tk-framework-aliastranslations_v0.x.x")
        if not framework:
            self.logger.warning("Couldn't find the Alias Translations Framework")
            return False

        tk_framework_aliastranslations = framework.import_module(
            "tk_framework_aliastranslations"
        )
        translator = tk_framework_aliastranslations.Translator(path, publish_path)

        # if we don't match valid conditions for the translator, exit
        if not translator.is_valid():
            self.logger.warning("Invalid conditions for translator.")
            return

        # if we don't have translator settings, we can't publish
        if not translator.translation_type:
            self.logger.warning(
                f"Couldn't find the translation type {translator.translator_type}."
            )
            return False

        if not translator.translator_path:
            self.logger.warning(f"Couldn't determine which translator to use.")
            return False

        if not os.path.exists(translator.translator_path):
            self.logger.warning(
                f"Translator path does not exist {translator.translator_path}."
            )
            return False
        self.logger.info(f"Translator in use: {translator.translator_path}.")

        # store the licensing information in the item properties so that the translation could be run in
        # background mode
        alias_info = alias_api.get_product_information()
        item.local_properties.license_settings = {
            "product_key": alias_info.get("product_key"),
            "product_version": alias_info.get("product_version"),
            "product_license_type": alias_info.get("product_license_type"),
            "product_license_path": alias_info.get("product_license_path"),
        }

        return super(AliasTranslationPublishPlugin, self).validate(settings, item)

    def publish(self, settings, item):
        """
        Executes the publish logic for the given item and settings.
        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        """

        # get the publish "mode" stored inside of the root item properties
        bg_processing = item.parent.parent.properties.get("bg_processing", False)
        in_bg_process = item.parent.parent.properties.get("in_bg_process", False)

        if not bg_processing or (bg_processing and in_bg_process):

            publisher = self.parent

            # get the path to create and publish
            publish_path = self.get_publish_path(settings, item)

            # ensure the publish folder exists:
            publish_folder = os.path.dirname(publish_path)
            self.parent.ensure_folder_exists(publish_folder)

            # need to build a new instance of the translator for each translation because the type is changing
            framework = self.load_framework("tk-framework-aliastranslations_v0.x.x")
            tk_framework_aliastranslations = framework.import_module(
                "tk_framework_aliastranslations"
            )
            translator = tk_framework_aliastranslations.Translator(
                item.properties.path, publish_path
            )

            # set the license information
            translator.translator_settings.license_settings = item.get_property(
                "license_settings"
            )

            if (
                settings.get("Translator Settings")
                and settings.get("Translator Settings").value
            ):
                for setting in settings.get("Translator Settings").value:
                    translator.add_extra_param(
                        setting.get("name"), setting.get("value")
                    )

            try:
                translator.execute()
            except Exception as e:
                self.logger.error("Failed to run translation: %s" % e)
                return

            parent_sg_publish_data = item.parent.properties.get("sg_publish_data")

            if parent_sg_publish_data and not item.description:
                item.description = parent_sg_publish_data["description"]

            super(AliasTranslationPublishPlugin, self).publish(settings, item)

            # If we have some parent publish data, share the thumbnail between the parent publish and it child
            if parent_sg_publish_data:
                request_timeout = 60
                start_time = time.perf_counter()
                self.logger.debug("Sharing the thumbnail")
                thumbnail_shared = False
                while time.perf_counter() - start_time <= request_timeout:
                    try:
                        publisher.shotgun.share_thumbnail(
                            entities=[item.properties.get("sg_publish_data")],
                            source_entity=parent_sg_publish_data,
                        )
                        self.logger.debug("Thumbnail shared successfully")
                        thumbnail_shared = True
                        break
                    except Exception as e:
                        pass
                    time.sleep(1)

                if not thumbnail_shared:
                    self.logger.debug("Thumbnail couln't be shared")

    def finalize(self, settings, item):
        """
        Execute the finalization pass. This pass executes once all the publish
        tasks have completed, and can for example be used to version up files.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        """

        # get the publish "mode" stored inside of the root item properties
        bg_processing = item.parent.parent.properties.get("bg_processing", False)
        in_bg_process = item.parent.parent.properties.get("in_bg_process", False)

        if not bg_processing or (bg_processing and in_bg_process):

            self.logger.info(
                "Translation published successfully",
                extra={
                    "action_show_in_shotgun": {
                        "label": "Show Publish",
                        "tooltip": "Reveal the published file in Flow Production Tracking.",
                        "entity": item.properties["sg_publish_data"],
                    }
                },
            )

    def get_publish_template(self, settings, item):
        """
        Get a publish template for the supplied settings and item.

        :param settings: This plugin instance's configured settings
        :param item: The item to determine the publish template for

        :return: A template representing the publish path of the item or
            None if no template could be identified.
        """

        publisher = self.parent

        # here we can't use the item.properties.publish_path value as it can store the current session publish template
        publish_template_setting = settings.get("Publish Template")
        publish_template = publisher.engine.get_template_by_name(
            publish_template_setting.value
        )

        return publish_template

    def get_publish_type(self, settings, item):
        """
        Get a publish type for the supplied settings and item.

        :param settings: This plugin instance's configured settings
        :param item: The item to determine the publish type for

        :return: A publish type or None if one could not be found.
        """

        publisher = self.parent

        # get the publish type from the publish path extension as the item will have the session publish type
        publish_path = self.get_publish_path(settings, item)

        path_info = publisher.util.get_file_path_components(publish_path)
        extension = path_info["extension"]

        # ensure lowercase and no dot
        if extension:
            extension = extension.lstrip(".").lower()

            for type_def in settings["File Types"].value:

                publish_type = type_def[0]
                file_extensions = type_def[1:]

                if extension in file_extensions:
                    # found a matching type in settings. use it!
                    return publish_type

        # --- no pre-defined publish type found...

        if extension:
            # publish type is based on extension
            publish_type = "%s File" % extension.capitalize()
        else:
            # no extension, assume it is a folder
            publish_type = "Folder"

        return publish_type

    def get_publish_name(self, settings, item):
        """
        Get the publish name for the supplied settings and item.

        :param settings: This plugin instance's configured settings
        :param item: The item to determine the publish name for

        Uses the path info hook to retrieve the publish name.
        """

        publisher = self.parent
        publish_path = self.get_publish_path(settings, item)

        return publisher.util.get_publish_name(publish_path, sequence=False)

    def _copy_work_to_publish(self, settings, item):
        """
        This method handles copying work file path(s) to a designated publish
        location.

        This method requires a "work_template" and a "publish_template" be set
        on the supplied item.

        The method will handle copying the "path" property to the corresponding
        publish location assuming the path corresponds to the "work_template"
        and the fields extracted from the "work_template" are sufficient to
        satisfy the "publish_template".

        The method will not attempt to copy files if any of the above
        requirements are not met. If the requirements are met, the file will
        ensure the publish path folder exists and then copy the file to that
        location.

        If the item has "sequence_paths" set, it will attempt to copy all paths
        assuming they meet the required criteria with respect to the templates.

        """

        # here, as we inherit from the publish_plugin, we have to remove all the actions done in _copy_work_to_publish
        # otherwise the translation will be erased by the wire work file
        pass


def _session_path():
    """
    Return the path to the current session
    :return:
    """

    return alias_api.get_current_path()


def _get_save_as_action():
    """Simple helper for returning a log action to show the "File Save As" dialog"""
    return {
        "action_button": {
            "label": "Save As...",
            "tooltip": "Save the current session",
            "callback": sgtk.platform.current_engine().open_save_as_dialog,
        }
    }

AliasPublishVariantsPlugin

# Copyright (c) 2017 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.

import os
import sgtk
import alias_api

HookBaseClass = sgtk.get_hook_baseclass()


class AliasPublishVariantsPlugin(HookBaseClass):
    """
    Plugin for publishing variants of the current alias open session
    """

    @property
    def name(self):
        """
        One line display name describing the plugin
        """
        return "Publish Variants to Flow Production Tracking"

    @property
    def description(self):
        return """
        <p>
            This plugin exports all Variant images created in Alias and makes a Note in Flow Production Tracking for each one.
        </p>
        <p>
            All Notes are linked this version & file. Use this to sync all review notes made in Alias with Flow Production Tracking.
        </p>
        <p>
            To see the Variant images that will be exported, check the Alias Variant Lister.
        </p>
        """

    @property
    def item_filters(self):
        """
        List of item types that this plugin is interested in.

        Only items matching entries in this list will be presented to the
        accept() method. Strings can contain glob patters such as *, for example
        ["maya.*", "file.maya"]
        """
        return ["alias.session"]

    def accept(self, settings, item):
        """
        Method called by the publisher to determine if an item is of any
        interest to this plugin. Only items matching the filters defined via the
        item_filters property will be presented to this method.

        A publish task will be generated for each item accepted here. Returns a
        dictionary with the following booleans:

            - accepted: Indicates if the plugin is interested in this value at
                all. Required.
            - enabled: If True, the plugin will be enabled in the UI, otherwise
                it will be disabled. Optional, True by default.
            - visible: If True, the plugin will be visible in the UI, otherwise
                it will be hidden. Optional, True by default.
            - checked: If True, the plugin will be checked in the UI, otherwise
                it will be unchecked. Optional, True by default.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process

        :returns: dictionary with boolean keys accepted, required and enabled
        """

        if not alias_api.has_variants():
            self.logger.debug("There are not variants to export")
            return {"accepted": False}

        return {"accepted": True, "checked": False}

    def validate(self, settings, item):
        """
        Validates the given item to check that it is ok to publish. Returns a
        boolean to indicate validity.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        :returns: True if item is valid, False otherwise.
        """

        return True

    def publish(self, settings, item):
        """
        Executes the publish logic for the given item and settings.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        """

        # get the publish "mode" stored inside of the root item properties
        bg_processing = item.parent.properties.get("bg_processing", False)
        in_bg_process = item.parent.properties.get("in_bg_process", False)

        # as the alias_api.get_variants() method doesn't work with OpenModel
        # we need to get the variants locally
        if not bg_processing or (bg_processing and not in_bg_process):
            variants = []
            for variant in alias_api.get_variants():
                variants.append((variant.name, variant.path))
            item.properties["alias_variants"] = variants

        if not bg_processing or (bg_processing and in_bg_process):

            publisher = self.parent
            version_data = item.properties.get("sg_version_data")
            publish_data = item.properties["sg_publish_data"]

            # Links, the note will be attached to published file by default
            # if a version is created the note will be attached to this too
            note_links = [publish_data]

            if version_data is not None:
                note_links.append(version_data)

            for variant in item.properties["alias_variants"]:
                data = {
                    "project": item.context.project,
                    "user": item.context.user,
                    "subject": "Alias Variant",
                    "content": variant[0],
                    "note_links": note_links,
                }
                if item.context.task:
                    data["tasks"] = [item.context.task]

                note = publisher.shotgun.create("Note", data)
                publisher.shotgun.upload_thumbnail(
                    entity_type="Note", entity_id=note.get("id"), path=variant[1]
                )
                variant_filepath = variant[1]
                _, file_ext = os.path.splitext(variant_filepath)

                publisher.shotgun.upload(
                    entity_type="Note",
                    entity_id=note.get("id"),
                    path=variant_filepath,
                    field_name="attachments",
                    display_name="{name}{ext}".format(name=variant[0], ext=file_ext),
                )

    def finalize(self, settings, item):
        """
        Execute the finalization pass. This pass executes once all the publish
        tasks have completed, and can for example be used to version up files.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        """

        # get the publish "mode" stored inside of the root item properties
        bg_processing = item.parent.properties.get("bg_processing", False)
        in_bg_process = item.parent.properties.get("in_bg_process", False)

        if not bg_processing or (bg_processing and in_bg_process):
            self.logger.info("Variants published successfully")

AliasStartVersionControlPlugin

# Copyright (c) 2017 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.

import os
import sgtk

import alias_api

HookBaseClass = sgtk.get_hook_baseclass()


class AliasStartVersionControlPlugin(HookBaseClass):
    """
    Simple plugin to insert a version number into the alias file path if one
    does not exist.
    """

    @property
    def icon(self):
        """
        Path to an png icon on disk
        """

        # look for icon one level up from this hook's folder in "icons" folder
        return os.path.join(self.disk_location, os.pardir, "icons", "version_up.png")

    @property
    def name(self):
        """
        One line display name describing the plugin
        """
        return "Begin file versioning"

    @property
    def description(self):
        """
        Verbose, multi-line description of what the plugin does. This can
        contain simple html for formatting.
        """
        return """
        Adds a version number to the filename.<br><br>

        Once a version number exists in the file, the publishing will
        automatically bump the version number. For example,
        <code>filename.ext</code> will be saved to
        <code>filename.v001.ext</code>.<br><br>

        If the session has not been saved, validation will fail and a button
        will be provided in the logging output to save the file.<br><br>

        If a file already exists on disk with a version number, validation will
        fail and the logging output will include button to save the file to a
        different name.<br><br>
        """

    @property
    def item_filters(self):
        """
        List of item types that this plugin is interested in.

        Only items matching entries in this list will be presented to the
        accept() method. Strings can contain glob patters such as *, for example
        ["maya.*", "file.maya"]
        """
        return ["alias.session"]

    @property
    def settings(self):
        """
        Dictionary defining the settings that this plugin expects to receive
        through the settings parameter in the accept, validate, publish and
        finalize methods.

        A dictionary on the following form::

            {
                "Settings Name": {
                    "type": "settings_type",
                    "default": "default_value",
                    "description": "One line description of the setting"
            }

        The type string should be one of the data types that toolkit accepts as
        part of its environment configuration.
        """
        return {}

    def accept(self, settings, item):
        """
        Method called by the publisher to determine if an item is of any
        interest to this plugin. Only items matching the filters defined via the
        item_filters property will be presented to this method.

        A publish task will be generated for each item accepted here. Returns a
        dictionary with the following booleans:

            - accepted: Indicates if the plugin is interested in this value at
                all. Required.
            - enabled: If True, the plugin will be enabled in the UI, otherwise
                it will be disabled. Optional, True by default.
            - visible: If True, the plugin will be visible in the UI, otherwise
                it will be hidden. Optional, True by default.
            - checked: If True, the plugin will be checked in the UI, otherwise
                it will be unchecked. Optional, True by default.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process

        :returns: dictionary with boolean keys accepted, required and enabled
        """

        path = _session_path()

        if path:
            version_number = self._get_version_number(path, item)
            if version_number is not None:
                self.logger.info(
                    "Alias '%s' plugin rejected the current session..." % (self.name,)
                )
                self.logger.info("  There is already a version number in the file...")
                self.logger.info("  Alias file path: %s" % (path,))
                return {"accepted": False}
        else:
            # the session has not been saved before (no path determined).
            # provide a save button. the session will need to be saved before
            # validation will succeed.
            self.logger.warn(
                "The Alias session has not been saved.",
                extra=_get_save_as_action(),
            )

        self.logger.info(
            "Alias '%s' plugin accepted the current session." % (self.name,),
            extra=_get_version_docs_action(),
        )

        # accept the plugin, but don't force the user to add a version number
        # (leave it unchecked)
        return {"accepted": True, "checked": False}

    def validate(self, settings, item):
        """
        Validates the given item to check that it is ok to publish.

        Returns a boolean to indicate validity.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process

        :returns: True if item is valid, False otherwise.
        """

        publisher = self.parent
        path = _session_path()

        if not path:
            # the session still requires saving. provide a save button.
            # validation fails
            error_msg = "The Alias session has not been saved."
            self.logger.error(error_msg, extra=_get_save_as_action())
            raise Exception(error_msg)

        # NOTE: If the plugin is attached to an item, that means no version
        # number could be found in the path. If that's the case, the work file
        # template won't be much use here as it likely has a version number
        # field defined within it. Simply use the path info hook to inject a
        # version number into the current file path

        # get the path to a versioned copy of the file.
        version_path = publisher.util.get_version_path(path, "v001")
        if os.path.exists(version_path):
            error_msg = (
                "A file already exists with a version number. Please "
                "choose another name."
            )
            self.logger.error(error_msg, extra=_get_save_as_action())
            raise Exception(error_msg)

        return True

    def publish(self, settings, item):
        """
        Executes the publish logic for the given item and settings.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        """

        publisher = self.parent

        # get the path in a normalized state. no trailing separator, separators
        # are appropriate for current os, no double separators, etc.
        path = sgtk.util.ShotgunPath.normalize(_session_path())

        # ensure the session is saved in its current state
        publisher.engine.save_file()

        # get the path to a versioned copy of the file.
        version_path = publisher.util.get_version_path(path, "v001")

        # save to the new version path
        publisher.engine.save_file_as(version_path)
        self.logger.info("A version number has been added to the Alias file...")
        self.logger.info("  Alias file path: %s" % (version_path,))

    def finalize(self, settings, item):
        """
        Execute the finalization pass. This pass executes once
        all the publish tasks have completed, and can for example
        be used to version up files.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        """
        pass

    def _get_version_number(self, path, item):
        """
        Try to extract and return a version number for the supplied path.

        :param path: The path to the current session

        :return: The version number as an `int` if it can be determined, else
            None.

        NOTE: This method will use the work template provided by the
        session collector, if configured, to determine the version number. If
        not configured, the version number will be extracted using the zero
        config path_info hook.
        """

        publisher = self.parent
        version_number = None

        work_template = item.properties.get("work_template")
        if work_template:
            if work_template.validate(path):
                self.logger.debug("Using work template to determine version number.")
                work_fields = work_template.get_fields(path)
                if "version" in work_fields:
                    version_number = work_fields.get("version")
            else:
                self.logger.debug("Work template did not match path")
        else:
            self.logger.debug("Work template unavailable for version extraction.")

        if version_number is None:
            self.logger.debug("Using path info hook to determine version number.")
            version_number = publisher.util.get_version_number(path)

        return version_number


def _session_path():
    """
    Return the path to the current session
    :return:
    """

    return alias_api.get_current_path()


def _get_save_as_action():
    """Simple helper for returning a log action to show the "File Save As" dialog"""
    return {
        "action_button": {
            "label": "Save As...",
            "tooltip": "Save the current session",
            "callback": sgtk.platform.current_engine().open_save_as_dialog,
        }
    }


def _get_version_docs_action():
    """
    Simple helper for returning a log action to show version docs
    """
    return {
        "action_open_url": {
            "label": "Version Docs",
            "tooltip": "Show docs for version formats",
            "url": "https://help.autodesk.com/view/SGSUB/ENU/?guid=SG_Supervisor_Artist_sa_integrations_sa_integrations_user_guide_html",
        }
    }

UploadVersionPlugin

# Copyright (c) 2017 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
import os
import shutil
import tempfile

import sgtk

HookBaseClass = sgtk.get_hook_baseclass()


class UploadVersionPlugin(HookBaseClass):
    """Plugin for uploading Versions to Flow Production Tracking for review."""

    # Version Type string constants
    VERSION_TYPE_2D = "2D Version"
    VERSION_TYPE_3D = "3D Version"

    # Version Type Options
    VERSION_TYPE_OPTIONS = [
        VERSION_TYPE_2D,
        VERSION_TYPE_3D,
    ]

    # Descriptions for Version Types
    VERSION_TYPE_DESCRIPTIONS = {
        VERSION_TYPE_2D: """
                Create a Version in Flow Production Tracking for Review.<br/><br/>
                A 2D Version (image or video representation of your file/scene) will be created in Flow Production Tracking.
                This Version can then be reviewed via Flow Production Tracking's many review apps.
            """,
        VERSION_TYPE_3D: """
                Create a Version in Flow Production Tracking for Review.<br/><br/>
                A 3D Version (LMV translation of your file/scene's geometry) will be created in
                Flow Production Tracking. This Version can then be reviewed via Flow Production Tracking's
                many review apps.<br/><br/> References in your file will not be included in the 3D version.
            """,
    }

    @property
    def icon(self):
        """Path to an png icon on disk."""

        return os.path.join(self.disk_location, os.pardir, "icons", "review.png")

    @property
    def settings(self):
        """
        Dictionary defining the settings that this plugin expects to recieve
        through the settings parameter in the accept, validate, publish and
        finalize methods.

        A dictionary on the following form::

            {
                "Settings Name": {
                    "type": "settings_type",
                    "default": "default_value",
                    "description": "One line description of the setting"
            }

        The type string should be one of the data types that toolkit accepts as
        part of its environment configuration.
        """
        # inherit the settings from the base publish plugin
        base_settings = super(UploadVersionPlugin, self).settings or {}

        # settings specific to this class
        upload_version_settings = {
            "Version Type": {
                "type": "str",
                "default": self.VERSION_TYPE_2D,
                "description": "Generate a {options} or {last_option} Version".format(
                    options=", ".join(self.VERSION_TYPE_OPTIONS[:-1]),
                    last_option=self.VERSION_TYPE_OPTIONS[-1],
                ),
            },
            "Upload": {
                "type": "bool",
                "default": False,
                "description": "Upload content to Flow Production Tracking?",
            },
        }

        # update the base settings
        base_settings.update(upload_version_settings)

        return base_settings

    @property
    def item_filters(self):
        """
        List of item types that this plugin is interested in.

        Only items matching entries in this list will be presented to the
        accept() method. Strings can contain glob patters such as *, for example
        ["maya.*", "file.maya"]
        """
        return ["alias.session"]

    def accept(self, settings, item):
        """
        Method called by the publisher to determine if an item is of any
        interest to this plugin. Only items matching the filters defined via the
        item_filters property will be presented to this method.

        A publish task will be generated for each item accepted here. Returns a
        dictionary with the following booleans:

            - accepted: Indicates if the plugin is interested in this value at
                all. Required.
            - enabled: If True, the plugin will be enabled in the UI, otherwise
                it will be disabled. Optional, True by default.
            - visible: If True, the plugin will be visible in the UI, otherwise
                it will be hidden. Optional, True by default.
            - checked: If True, the plugin will be checked in the UI, otherwise
                it will be unchecked. Optional, True by default.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process

        :returns: dictionary with boolean keys accepted, required and enabled
        """

        return {"accepted": True, "checked": True}

    def validate(self, settings, item):
        """
        Validates the given item to check that it is ok to publish. Returns a
        boolean to indicate validity.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        :returns: True if item is valid, False otherwise.
        """

        path = item.get_property("path")
        if not path:
            self.logger.error("No path found for item")
            return False

        # Validate fails if the Version Type is not supported
        version_type = settings.get("Version Type").value
        if version_type not in self.VERSION_TYPE_OPTIONS:
            self.logger.error("Unsupported Version Type '{}'".format(version_type))
            return False

        # Check the site pref for 3D Review enabled. Provide warning messages if the user is
        # attempting to create a 3D Version but may not have 3D Review enabled on their site, but
        # do not block the user from publishing.
        if version_type == self.VERSION_TYPE_3D:
            is_3d_viewer_enabled = self._is_3d_viewer_enabled()
            if is_3d_viewer_enabled is None:
                self.logger.warning(
                    "Failed to check if 3D Review is enabled for your site."
                )
                self.logger.warning(
                    "Please contact Autodesk support to access your site preference for 3D Review or use the 2D Version publish option instead."
                )
            elif not is_3d_viewer_enabled:
                self.logger.warning("Your site does not have 3D Review enabled.")
                self.logger.warning(
                    "Please contact Autodesk support to have 3D Review enabled on your Flow Production Tracking site or use the 2D Version publish option instead."
                )

            framework_lmv = self.load_framework("tk-framework-lmv_v1.x.x")
            if not framework_lmv:
                self.logger.error("Missing required framework tk-framework-lmv v1.x.x")
                return False

            translator = framework_lmv.import_module("translator")
            lmv_translator = translator.LMVTranslator(
                path, self.parent.sgtk, item.context
            )
            lmv_translator_path = lmv_translator.get_translator_path()
            if not lmv_translator_path:
                self.logger.error(
                    "Missing translator for Alias. Alias must be installed locally to run LMV translation."
                )
                return False

        return True

    def publish(self, settings, item):
        """
        Executes the publish logic for the given item and settings.
        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        """

        # Get the publish "mode" stored inside of the root item properties
        bg_processing = item.parent.properties.get("bg_processing", False)
        in_bg_process = item.parent.properties.get("in_bg_process", False)

        if not bg_processing or (bg_processing and in_bg_process):

            publisher = self.parent
            path = item.properties["path"]

            # Be sure to strip the extension from the publish name
            path_components = publisher.util.get_file_path_components(path)
            filename = path_components["filename"]
            (publish_name, _) = os.path.splitext(filename)
            item.properties["publish_name"] = publish_name

            # Create the Version in Flow Production Tracking
            super(UploadVersionPlugin, self).publish(settings, item)

            # Generate media content and upload to Flow Production Tracking
            version_type = item.properties["sg_version_data"]["type"]
            version_id = item.properties["sg_version_data"]["id"]
            thumbnail_path = item.get_thumbnail_as_path()
            media_package_path = None
            media_version_type = settings.get("Version Type").value
            if media_version_type == self.VERSION_TYPE_3D:
                # Pass the thumbnail retrieved to override the LMV thumbnail, and ignore the
                # LMV thumbnail output
                media_package_path, _, _ = self._translate_file_to_lmv(
                    item, thumbnail_path=thumbnail_path
                )
                self.logger.info("Translated file to LMV")

            if media_package_path:
                # For 3D media, a media package path will be generated. Set the translation
                # type on the Version in order to view 3D media in Flow Production Tracking Web.
                self.parent.shotgun.update(
                    entity_type=version_type,
                    entity_id=version_id,
                    data={"sg_translation_type": "LMV"},
                )
                self.logger.info("Set Version translation type to LMV")

            uploaded_movie_path = media_package_path or thumbnail_path
            if uploaded_movie_path:
                # Uplod to the `sg_uploaded_movie` field on the Version so that the Version
                # thumbnail shows the "play" button on hover from Flow Production Tracking Web
                self.parent.shotgun.upload(
                    entity_type=version_type,
                    entity_id=version_id,
                    path=uploaded_movie_path,
                    field_name="sg_uploaded_movie",
                )
                self.logger.info(
                    f"Uploaded Version media from path {uploaded_movie_path}"
                )

            if thumbnail_path:
                self.parent.shotgun.upload_thumbnail(
                    entity_type=version_type,
                    entity_id=version_id,
                    path=thumbnail_path,
                )
                self.logger.info(
                    f"Uploaded Version thumbnail from path {thumbnail_path}"
                )

            # Remove the temporary directory or files created to generate media content
            self._cleanup_temp_files(media_package_path)

    def finalize(self, settings, item):
        """
        Execute the finalization pass. This pass executes once all the publish
        tasks have completed, and can for example be used to version up files.

        :param settings: Dictionary of Settings. The keys are strings, matching
            the keys returned in the settings property. The values are `Setting`
            instances.
        :param item: Item to process
        """

        # get the publish "mode" stored inside of the root item properties
        bg_processing = item.parent.properties.get("bg_processing", False)
        in_bg_process = item.parent.properties.get("in_bg_process", False)

        if not bg_processing or (bg_processing and in_bg_process):
            super(UploadVersionPlugin, self).finalize(settings, item)

    ############################################################################
    # Methods for creating/displaying custom plugin interface

    def create_settings_widget(self, parent, items=None):
        """
        Creates a Qt widget, for the supplied parent widget (a container widget
        on the right side of the publish UI).

        :param parent: The parent to use for the widget being created.
        :param items: A list of PublishItems the selected publish tasks are parented to.
        :return: A QtGui.QWidget or subclass that displays information about
            the plugin and/or editable widgets for modifying the plugin's
            settings.
        """

        # defer Qt-related imports
        from sgtk.platform.qt import QtCore, QtGui

        # The main widget
        widget = QtGui.QWidget(parent)
        widget_layout = QtGui.QVBoxLayout()

        # create a group box to display the description
        description_group_box = QtGui.QGroupBox(widget)
        description_group_box.setTitle("Description:")

        # Defer setting the description text, this will be updated when
        # the version type combobox value is changed
        description_label = QtGui.QLabel()
        description_label.setWordWrap(True)
        description_label.setOpenExternalLinks(True)
        description_label.setTextFormat(QtCore.Qt.RichText)

        # create the layout to use within the group box
        description_layout = QtGui.QVBoxLayout()
        description_layout.addWidget(description_label)
        description_layout.addStretch()
        description_group_box.setLayout(description_layout)

        # Add a combobox to edit the version type option
        version_type_combobox = QtGui.QComboBox(widget)
        version_type_combobox.setAccessibleName("Version type selection dropdown")
        version_type_combobox.addItems(self.VERSION_TYPE_OPTIONS)
        # Hook up the signal/slot to update the description according to the current version type
        version_type_combobox.currentIndexChanged.connect(
            lambda index: self._on_version_type_changed(
                version_type_combobox.currentText(), description_label
            )
        )

        # Add all the minor widgets to the main widget
        widget_layout.addWidget(description_group_box)
        widget_layout.addWidget(version_type_combobox)
        widget.setLayout(widget_layout)

        # Set the widget property to store the combobox to access in get_ui_settings and set_ui_settings
        widget.setProperty("description_label", description_label)
        widget.setProperty("version_type_combobox", version_type_combobox)

        return widget

    def get_ui_settings(self, widget, items=None):
        """
        This method is required to be defined in order for the custom UI to show up in the app.

        Invoked by the Publisher when the selection changes. This method gathers the settings
        on the previously selected task, so that they can be later used to repopulate the
        custom UI if the task gets selected again. They will also be passed to the accept, validate,
        publish and finalize methods, so that the settings can be used to drive the publish process.

        The widget argument is the widget that was previously created by
        `create_settings_widget`.

        The method returns a dictionary, where the key is the name of a
        setting that should be updated and the value is the new value of that
        setting. Note that it is up to you how you want to store the UI's state as
        settings and you don't have to necessarily to return all the values from
        the UI. This is to allow the publisher to update a subset of settings
        when multiple tasks have been selected.

        Example::

            {
                 "setting_a": "/path/to/a/file"
            }

        :param widget: The widget that was created by `create_settings_widget`
        """

        ui_settings = {}

        # Get the Version Type settings value from the UI combobox
        version_type_combobox = widget.property("version_type_combobox")
        if version_type_combobox:
            version_type_index = version_type_combobox.currentIndex()
            if 0 <= version_type_index < len(self.VERSION_TYPE_OPTIONS):
                self.VERSION_TYPE_OPTIONS[version_type_index]
                ui_settings["Version Type"] = self.VERSION_TYPE_OPTIONS[
                    version_type_index
                ]
            else:
                self.logger.debug(
                    "Invalid Version Type index {}".format(version_type_index)
                )

        return ui_settings

    def set_ui_settings(self, widget, settings, items=None):
        """
        This method is required to be defined in order for the custom UI to show up in the app.

        Allows the custom UI to populate its fields with the settings from the
        currently selected tasks.

        The widget is the widget created and returned by
        `create_settings_widget`.

        A list of settings dictionaries are supplied representing the current
        values of the settings for selected tasks. The settings dictionaries
        correspond to the dictionaries returned by the settings property of the
        hook.

        Example::

            settings = [
            {
                 "seeting_a": "/path/to/a/file"
                 "setting_b": False
            },
            {
                 "setting_a": "/path/to/a/file"
                 "setting_b": False
            }]

        The default values for the settings will be the ones specified in the
        environment file. Each task has its own copy of the settings.

        When invoked with multiple settings dictionaries, it is the
        responsibility of the custom UI to decide how to display the
        information. If you do not wish to implement the editing of multiple
        tasks at the same time, you can raise a ``NotImplementedError`` when
        there is more than one item in the list and the publisher will inform
        the user than only one task of that type can be edited at a time.

        :param widget: The widget that was created by `create_settings_widget`.
        :param settings: a list of dictionaries of settings for each selected
            task.
        :param items: A list of PublishItems the selected publish tasks are parented to.
        """

        if not settings:
            return

        if len(settings) > 1:
            raise NotImplementedError

        version_type_combobox = widget.property("version_type_combobox")
        if not version_type_combobox:
            self.logger.debug(
                "Failed to retrieve Version Type combobox to set custom UI"
            )
            return

        description_label = widget.property("description_label")
        if not description_label:
            self.logger.debug(
                "Failed to retrieve Version Type combobox to set custom UI"
            )

        # Get the default setting for version type
        default_value = self.settings.get("Version Type", {}).get(
            "default", self.VERSION_TYPE_OPTIONS[0]
        )

        # Get the version type value from the settings, and set the combobox accordingly
        version_type_value = settings[0].get("Version Type", default_value)
        version_type_index = max(self.VERSION_TYPE_OPTIONS.index(version_type_value), 0)
        # Set the version type combobox
        current_version_index = version_type_combobox.currentIndex()
        if current_version_index == version_type_index:
            # Combobox already has the correct verstion type - manually trigger the 'currentIndexChanged'
            # slot to update the description label based on the version
            self._on_version_type_changed(version_type_value, description_label)
        else:
            version_type_combobox.setCurrentIndex(version_type_index)

    def _on_version_type_changed(self, version_type, description_label):
        """
        Slot called when the Version Type combobox selector index changes.

        Update the description based on the current Version Type.

        :param version_type: The current Version Type.
        :type version_type: str
        :param description_label: The label widget to set the description on
        :type description_label: QLabel
        """

        if not description_label:
            return

        note = ""
        if version_type == self.VERSION_TYPE_3D:
            is_3d_viewer_enabled = self._is_3d_viewer_enabled()

            if is_3d_viewer_enabled is None:
                note = """
                    <br/><br/>
                    <b>NOTE:</b>
                    <br/>
                    <b>
                        Failed to check if 3D Review is enabled for your site.
                    <br/>
                        You may create a 3D Version for review, but if 3D Review is not enabled,
                        you will see an error message 'No web playable media available' when trying to open the Version with the Media viewer.
                    </b>
                    <br/><br/>
                    <b>
                        Please contact Autodesk support to access your site preference for 3D Review or use the 2D Version publish option instead.
                    </b>
                """
            elif not is_3d_viewer_enabled:
                note = """
                    <br/><br/>
                    <b>NOTE:</b>
                    <br/>
                    <b>
                       Your site does not have 3D Review enabled.
                        <br/>
                       You may create a 3D Version for review, but until your site has 3D Review enabled,
                       you will see an error message 'No web playable media available' when trying to open the Version with the Media viewer.
                    </b>
                    <br/><br/>
                    <b>
                        Please contact Autodesk support to have 3D Review enabled on your Flow Production Tracking site or use the 2D Version publish option instead.
                    </b>
                """

        text = "{description}{note}".format(
            description=self.VERSION_TYPE_DESCRIPTIONS.get(
                version_type, self.description
            ),
            note=note,
        )
        description_label.setText(text)

    ############################################################################
    # Protected functions

    def _cleanup_temp_files(self, path, remove_from_root=True):
        """
        Remove any temporary directories or files from the given path.

        If `remove_from_root` is True, the top most level directory of the given path is
        used to remove all sub directories and files.

        :param path: The file path to remove temporary files and/or directories from.
        :type path: str
        :param remove_from_root: True will remove directories and files from the top most level
            directory within the root temporary directory, else False will remove the single
            file or directory (and its children). Default is True.
        :type remove_from_root: bool
        """

        if path is None or not os.path.exists(path):
            return  # Cannot clean up a path that does not exist

        tempdir = tempfile.gettempdir()
        if os.path.commonpath([path, tempdir]) != tempdir:
            return  # Not a temporary directory or file

        if remove_from_root:
            # Get the top most level of the path that is inside the root temp dir
            relative_path = os.path.relpath(path, tempdir)
            path = os.path.normpath(
                os.path.join(tempdir, relative_path.split(os.path.sep)[0])
            )

        if os.path.isdir(path):
            shutil.rmtree(path)
        elif os.path.isfile(path):
            os.remove(path)

    def _translate_file_to_lmv(self, item, thumbnail_path=None):
        """
        Translate the current Alias file as an LMV package in order to upload it to Flow Production Tracking as a 3D Version

        :param item: Item to process
        :type item: PublishItem
        :param thumbnail_path: Optionally pass a thumbnail file path to override the LMV
            thumbnail (this thumbnail will be included in the LMV packaged zip file).
        :type thumbnail_path: str

        :returns:
            - The path to the LMV zip file
            - The path to the LMV thumbnail
            - The path to the temporary folder where the LMV files have been processed
        """

        path = item.get_property("path")
        thumbnail_path = thumbnail_path or item.get_thumbnail_as_path()

        # Translate the file to LMV
        framework_lmv = self.load_framework("tk-framework-lmv_v1.x.x")
        translator = framework_lmv.import_module("translator")
        lmv_translator = translator.LMVTranslator(path, self.parent.sgtk, item.context)
        lmv_translator.translate()

        # Package up the LMV files into a zip file
        file_name = str(item.properties["sg_version_data"]["id"])
        package_path, lmv_thumbnail_path = lmv_translator.package(
            svf_file_name=file_name,
            thumbnail_path=thumbnail_path,
        )

        return package_path, lmv_thumbnail_path, lmv_translator.output_directory

    def _is_3d_viewer_enabled(self):
        """
        Look up the Flow Production Tracking site preference to check if the 3D Viewer is enabled. Return True
        if the 3D Viewer is enabled, False if it is disabled, or None if the 3D Viewer Enabled
        site pref could not be accessed.

        If the Flow Production Tracking API returns an empty dictionary, the hidden site preference could not be
        accessed. Ensure that the hidden site preference "API hidden allowed list of preferences"
        contains the "enable_3d_viewer" in its list.

        :return: True if the 3D Viewer is enabled for the Flow Production Tracking site, False if it is disabled,
                 or None if the 3D Viewer site pref could not be accessed.
        :rtype: bool
        """

        enable_3d_viewer_pref = "enable_3d_viewer"
        prefs = self.parent.shotgun.preferences_read(prefs=[enable_3d_viewer_pref])

        if not prefs:
            # The 'enable_3d_viewer' site pref could not be accessed
            return None

        return prefs[enable_3d_viewer_pref]

tk-multi-shotgunpanel

AliasActions

# Copyright (c) 2015 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.

"""
Hook that loads defines all the available actions, broken down by publish type.
"""

import os
import sgtk
import alias_api
import tempfile

HookBaseClass = sgtk.get_hook_baseclass()


class AliasActions(HookBaseClass):
    """
    Flow Production Tracking Panel Actions for Alias
    """

    def generate_actions(self, sg_data, actions, ui_area):
        """
        Returns a list of action instances for a particular publish.
        This method is called each time a user clicks a publish somewhere in the UI.
        The data returned from this hook will be used to populate the actions menu for a publish.

        The mapping between Publish types and actions are kept in a different place
        (in the configuration) so at the point when this hook is called, the loader app
        has already established *which* actions are appropriate for this object.

        The hook should return at least one action for each item passed in via the
        actions parameter.

        This method needs to return detailed data for those actions, in the form of a list
        of dictionaries, each with name, params, caption and description keys.

        Because you are operating on a particular publish, you may tailor the output
        (caption, tooltip etc) to contain custom information suitable for this publish.

        The ui_area parameter is a string and indicates where the publish is to be shown.
        - If it will be shown in the main browsing area, "main" is passed.
        - If it will be shown in the details area, "details" is passed.
        - If it will be shown in the history area, "history" is passed.

        Please note that it is perfectly possible to create more than one action "instance" for
        an action! You can for example do scene introspection - if the action passed in
        is "character_attachment" you may for example scan the scene, figure out all the nodes
        where this object can be attached and return a list of action instances:
        "attach to left hand", "attach to right hand" etc. In this case, when more than
        one object is returned for an action, use the params key to pass additional
        data into the run_action hook.

        :param sg_data: Flow Production Tracking data dictionary with all the standard publish fields.
        :param actions: List of action strings which have been defined in the app configuration.
        :param ui_area: String denoting the UI Area (see above).
        :returns List of dictionaries, each with keys name, params, caption and description
        """

        self.logger.debug(
            "Generate actions called for UI element %s. "
            "Actions: %s. Publish Data: %s" % (ui_area, actions, sg_data)
        )

        action_instances = []
        try:
            # call base class first
            action_instances += HookBaseClass.generate_actions(
                self, sg_data, actions, ui_area
            )
        except AttributeError:
            # base class doesn't have the method, so ignore and continue
            pass

        if "reference" in actions:
            action_instances.append(
                {
                    "name": "reference",
                    "params": None,
                    "caption": "Create Reference",
                    "description": "This will add the item to the universe as a standard reference.",
                }
            )

        if "import" in actions:
            action_instances.append(
                {
                    "name": "import",
                    "params": None,
                    "caption": "Import into Scene",
                    "description": "This will import the item into the current universe.",
                }
            )

        if "import_as_reference" in actions:
            action_instances.append(
                {
                    "name": "import_as_reference",
                    "params": None,
                    "caption": "Import as Reference",
                    "description": "This will import the item as a reference into the current universe.",
                }
            )

        if "texture_node" in actions:
            action_instances.append(
                {
                    "name": "texture_node",
                    "params": None,
                    "caption": "Create Canvas",
                    "description": "This will import the item into the current universe.",
                }
            )

        if "import_note_attachments" in actions:
            action_instances.append(
                {
                    "name": "import_note_attachments",
                    "params": None,
                    "caption": "Import Note attachment(s) as canvas image(s)",
                    "description": "This will create a new canvas for each image attached to the note.",
                }
            )

        if "import_subdiv" in actions:
            action_instances.append(
                {
                    "name": "import_subdiv",
                    "params": None,
                    "caption": "Import Subdiv file into Scene",
                    "description": "This will import the subdiv item into the current universe.",
                }
            )

        return action_instances

    def execute_action(self, name, params, sg_data):
        """
        Execute a given action. The data sent to this be method will
        represent one of the actions enumerated by the generate_actions method.

        :param name: Action name string representing one of the items returned by generate_actions.
        :param params: Params data, as specified by generate_actions.
        :param sg_data: Flow Production Tracking data dictionary with all the standard publish fields.
        :returns: No return value expected.
        """

        self.logger.debug(
            "Execute action called for action %s. "
            "Parameters: %s. Flow Production Tracking Data: %s"
            % (name, params, sg_data)
        )

        if name == "reference":
            path = self.get_publish_path(sg_data)
            self._create_reference(path)

        elif name == "import":
            path = self.get_publish_path(sg_data)
            self._import_file(path)

        elif name == "import_as_reference":
            path = self.get_publish_path(sg_data)
            self._import_file_as_reference(path)

        elif name == "texture_node":
            path = self.get_publish_path(sg_data)
            self._create_texture_node(path)

        if name == "import_note_attachments":
            self._import_note_attachments_as_canvas(sg_data)

        elif name == "import_subdiv":
            path = self.get_publish_path(sg_data)
            self._import_subdivision(path)

        else:
            try:
                HookBaseClass.execute_action(self, name, params, sg_data)
            except AttributeError:
                # base class doesn't have the method, so ignore and continue
                pass

    def execute_multiple_actions(self, actions):
        """
        Executes the specified action on a list of items.

        The default implementation dispatches each item from ``actions`` to
        the ``execute_action`` method.

        The ``actions`` is a list of dictionaries holding all the actions to execute.
        Each entry will have the following values:

            name: Name of the action to execute
            sg_data: Publish information coming from Flow Production Tracking
            params: Parameters passed down from the generate_actions hook.

        .. note::
            This is the default entry point for the hook. It reuses the ``execute_action``
            method for backward compatibility with hooks written for the previous
            version of the loader.

        .. note::
            The hook will stop applying the actions on the selection if an error
            is raised midway through.

        :param list actions: Action dictionaries.
        """
        for single_action in actions:
            name = single_action["name"]
            sg_data = single_action["sg_data"]
            params = single_action["params"]
            self.execute_action(name, params, sg_data)

    def _create_reference(self, path):
        """
        Create an Alias reference.

        :param path: Path to the file.
        """
        if not os.path.exists(path):
            raise Exception("File not found on disk - '%s'" % path)
        alias_api.create_reference(path)

    def _import_file(self, path):
        """
        Import the file into the current Alias session.

        :param path: Path to file.
        """
        if not os.path.exists(path):
            raise Exception("File not found on disk - '%s'" % path)
        alias_api.import_file(path)

    def _import_file_as_reference(self, path):
        """
        Import the file as an Alias reference, converting it on the fly as wref.

        :param path: Path to the file.
        """

        reference_template = self.parent.engine.get_template("reference_template")
        source_template = self.sgtk.template_from_path(path)

        # get the path to the reference, using the templates if it's possible otherwise using the source path
        # location
        if reference_template and source_template:
            template_fields = source_template.get_fields(path)
            template_fields["alias.extension"] = os.path.splitext(path)[-1][1:]
            reference_path = reference_template.apply_fields(template_fields)
        else:
            output_path, output_ext = os.path.splitext(path)
            reference_path = "{output_path}_{output_ext}.wref".format(
                output_path=output_path, output_ext=output_ext[1:]
            )

        # if the reference file doesn't exist on disk yet, run the translation
        if not os.path.exists(reference_path):

            framework = self.load_framework("tk-framework-aliastranslations_v0.x.x")
            if not framework:
                raise Exception("Couldn't find tk-framework-aliastranslations_v0.x.x")
            tk_framework_aliastranslations = framework.import_module(
                "tk_framework_aliastranslations"
            )

            translator = tk_framework_aliastranslations.Translator(path, reference_path)
            translator.execute()

        alias_api.create_reference(reference_path)

    def _create_texture_node(self, path):
        """
        Import an image as Canvas in Alias

        :param path:  Path to the image.
        """
        if not os.path.exists(path):
            raise Exception("File not found on disk - '%s'" % path)
        alias_api.create_texture_node(path, True)

    def _import_note_attachments_as_canvas(self, sg_data):
        """
        Import the Note attachments as canvas images.

        This will create a new canvas for each image attached to the note.

        :param sg_data: The Flow Production Tracking entity dict for the note.
        :type sg_data: dict
        """

        if not sg_data or not sg_data.get("id"):
            return

        sg_note = self.parent.shotgun.find_one(
            "Note", [["id", "is", sg_data["id"]]], ["attachments"]
        )
        if not sg_note:
            return

        with tempfile.TemporaryDirectory() as temp_dir:
            for attachment in sg_note["attachments"]:
                temp_path = os.path.join(temp_dir, attachment["name"])
                self.parent.shotgun.download_attachment(attachment, temp_path)
                alias_api.create_texture_node(temp_path)

    def _import_subdivision(self, path):
        """
        Import a file as subdivision in the current Alias session.

        :param path: Path to the file.
        """
        if not os.path.exists(path):
            raise Exception("File not found on disk - '%s'" % path)

        try:
            alias_api.import_subdiv(path)
        except alias_api.AliasPythonException as api_error:
            err_msg = "{api_error}<br/><br/>For more information, click {help_link}.".format(
                api_error=str(api_error),
                help_link="<a href='https://help.autodesk.com/view/ALIAS/2023/ENU/?guid=GUID-667410AD-CF4D-43B3-AE96-0C1331CB80B2'>here</a>",
            )
            raise alias_api.AliasPythonException(err_msg)

tk-multi-workfiles2

SceneOperation

# Copyright (c) 2017 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.

import alias_api
import uuid

import sgtk
from sgtk.platform.qt import QtGui

HookClass = sgtk.get_hook_baseclass()


class SceneOperation(HookClass):
    """
    Hook called to perform an operation with the
    current file
    """

    def execute(
        self,
        operation,
        file_path,
        context=None,
        parent_action=None,
        file_version=None,
        read_only=None,
        **kwargs
    ):
        """
        Main hook entry point

        :param operation:       String
                                Scene operation to perform

        :param file_path:       String
                                File path to use if the operation
                                requires it (e.g. open)

        :param context:         Context
                                The context the file operation is being
                                performed in.

        :param parent_action:   This is the action that this scene operation is
                                being executed for.  This can be one of:
                                - open_file
                                - new_file
                                - save_file_as
                                - version_up

        :param file_version:    The version/revision of the file to be opened.  If this is 'None'
                                then the latest version should be opened.

        :param read_only:       Specifies if the file should be opened read-only or not

        :returns:               Depends on operation:
                                'current_path' - Return the current scene
                                                 file path as a String
                                'open'         - True if file was opened, otherwise False
                                'reset'        - True if scene was reset to an empty
                                                 state, otherwise False
                                all others     - None
        """

        # At the end of the scene operation, indicate if the context needs to be saved for the
        # current Alias stage.
        save_context = operation in ["save_as", "prepare_new", "open", "reset"]

        # Use the event watcher context manager to queue any callbacks triggered by Alias
        # events while performing any scene operations. This ensures that all Alias file I/O
        # operations are complete before executing any event callbacks that may interfere
        # with Alias
        with self.parent.engine.event_watcher.create_context_manager():
            try:
                if operation == "current_path":
                    return alias_api.get_current_path()

                if operation == "open":
                    if alias_api.is_empty_file():
                        alias_api.open_file(file_path, new_stage=False)
                    else:
                        open_in_current_stage = (
                            self.parent.engine.open_delete_stages_dialog()
                        )
                        if open_in_current_stage == QtGui.QMessageBox.Cancel:
                            # Do not save the context if the operation was cancelled.
                            save_context = False
                            return False

                        if open_in_current_stage == QtGui.QMessageBox.No:
                            alias_api.open_file(file_path, new_stage=True)
                        else:
                            alias_api.reset()
                            alias_api.open_file(file_path, new_stage=False)

                elif operation == "save":
                    alias_api.save_file()

                elif operation == "save_as":
                    alias_api.save_file_as(file_path)

                elif operation == "reset":
                    # do not reset the file if we try to open another one as we have to deal with the stages an resetting
                    # the current session will delete all the stages
                    if parent_action == "open_file":
                        save_context = False
                        return True

                    if alias_api.is_empty_file() and len(alias_api.get_stages()) == 1:
                        alias_api.reset()
                        return True

                    open_in_current_stage = (
                        self.parent.engine.open_delete_stages_dialog(new_file=True)
                    )
                    if open_in_current_stage == QtGui.QMessageBox.Cancel:
                        # Do not save the context if the operation was cancelled.
                        save_context = False
                        return False

                    if open_in_current_stage == QtGui.QMessageBox.No:
                        stage_name = uuid.uuid4().hex
                        alias_api.create_stage(stage_name)
                    else:
                        alias_api.reset()

                    return True
            finally:
                if save_context:
                    # It is important that this method is executed before the event watcher
                    # context manager exits to ensure that the current context is saved for
                    # this Alias stage, before any event callbacks are triggered (whcih may
                    # require the contexts to be updated).
                    self.parent.engine.save_context_for_stage(context)