コード例 #1
0
# 
# 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 sgtk
import os

HookBaseClass = sgtk.get_hook_baseclass()

class MaxActions(HookBaseClass):

    ##############################################################################################################
    # public interface - to be overridden by deriving classes 
    
    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.
コード例 #2
0
# not expressly granted therein are reserved by Shotgun Software Inc.

"""
Hook that loads defines all the available actions, broken down by publish type.
"""
import sgtk
import os
from sgtk.platform.qt import QtGui

from photoshop import (
    RemoteObject,
    app as ph_app,
)  # Deambiguate code referencing toolkit app and Photoshop app.
from photoshop.flexbase import requestStatic

HookBaseClass = sgtk.get_hook_baseclass()

# Name of available actions. Corresponds to both the environment config values and the action instance names.
_ADD_AS_A_LAYER = "add_as_a_layer"
_OPEN_FILE = "open_file"


class PhotoshopActions(HookBaseClass):

    ##############################################################################################################
    # public interface - to be overridden by deriving classes

    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.
コード例 #3
0
# 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 glob
import re

import sgtk
from sgtk import TankError

hook = sgtk.get_hook_baseclass()

import nuke


class ScanSceneHook(hook):
    """
    Hook to scan scene for external files to copy
    """
    
    #def execute(self, app_instance, **kwargs):
    def execute(self, **kwargs):
        """
        Main hook entry point
        :returns:   
                    references_scanned: int
コード例 #4
0
ファイル: bootstrap_hook.py プロジェクト: wwfxuk/tk-core
class Bootstrap(get_hook_baseclass()):
    """
    This hook allows to download certain bundles from Shotgun instead of
    official source for the bundle.
    """
    def can_cache_bundle(self, descriptor):
        """
        Returns true if the descriptor has been cached in Shotgun.

        :param descriptor: Descriptor of the bundle that needs to be cached.
        :type descriptor: :class:`~sgtk.descriptor.Descriptor`

        :returns: ``True`` if the bundle is cached in Shotgun, ``False`` otherwise.
        """
        return bool(descriptor.get_dict()["type"] in ["app_store", "git"]
                    and self._get_bundle_attachment(descriptor))

    def _get_bundle_attachment(self, descriptor):
        """
        Retrieves the attachment associated to this descriptor in Shotgun.

        :param descriptor: Descriptor of the bundle that needs to be cached.
        :type descriptor: :class:`~sgtk.descriptor.Descriptor`

        :returns: The attachment entity dict.
        :rtype: dict
        """
        # This method gets invoked twice by the bootstrap hook, but will only be
        # invoked if the bundle is missing from disk so it is not worth putting a
        # caching system in place for the method.

        # We're using the descriptor uri, i.e.
        # sgtk:descriptor:app_store?name=tk-core&version=v0.18.150,
        # as the code for the entity. If an entry is found and there is an
        # attachment, the bundle can be downloaded from Shotgun.
        #
        # You could write a script that introspects a configuration,
        # extracts all bundles that need to be cached into Shotgun and pushes
        # them to Shotgun. Part of this workflow can be automated with the
        # ``developer/populate_bundle_cache.py`` script, which will download
        # locally every single bundle for a given config.
        entity = self.shotgun.find_one(
            "CustomNonProjectEntity01",
            [["sg_descriptor", "is",
              descriptor.get_uri()]], ["sg_content"])
        if entity:
            return entity["sg_content"]
        else:
            return None

    def populate_bundle_cache_entry(self, destination, descriptor, **kwargs):
        """
        This method will retrieve the bundle from the Shotgun site and unpack it
        in the destination folder.

        :param destination: Folder where the bundle needs to be written. Note
            that this is not the final destination folder inside the bundle
            cache.

        :param descriptor: Descriptor of the bundle that needs to be cached.
        :type descriptor: :class:`~sgtk.descriptor.Descriptor`
        """
        attachment = self._get_bundle_attachment(descriptor)
        download_and_unpack_attachment(self.shotgun, attachment, destination)
        self.logger.info("Bundle %s was downloaded from %s.",
                         descriptor.get_uri(), self.shotgun.base_url)
コード例 #5
0
# not expressly granted therein are reserved by Shotgun Software Inc.

import os
import mock

from publish_api_test_base import PublishApiTestBase
from tank_test.tank_test_base import setUpModule # noqa

import sgtk

# When this file is being loaded by the the unit test framework, there is no hook base
# class, so we'll skip the hook declaration in that case.
# When we call create_hook_instance during the test however, this file will be parsed
# again and this time get_hook_baseclass will return something.
try:
    base_class = sgtk.get_hook_baseclass()
except Exception:
    pass
else:
    class TestHook(base_class):

        def test_get_user_settings(self, test, settings, item):

            # Make sure get_publish_user returns nothing by default.
            test.assertIsNone(self.get_publish_user({}, item))

            # If the publish_user is set, get_publish_user should return it.
            user_in_property = {"type": "HumanUser", "id": 1}
            item.properties.publish_user = user_in_property
            test.assertEqual(self.get_publish_user({}, item), user_in_property)
コード例 #6
0
class PostInstall(sgtk.get_hook_baseclass()):
    """
    Ran after this desktop version is installed. It is run to make sure that
    the Shotgun API version is the one bundled with core v0.16.0 or higher.
    This test is required because the Desktop app historically didn't restart
    the Desktop app after updating core. Because of this, upgrading to core
    v0.16.0 meant that the wrong version of shotgun_api3 would be in memory.
    Therefore, even though the code was updated and the newer engine downloaded,
    we were actually still running the old code. This causes a crash in the
    tk-desktop engine because it expects the shotgun_api3 module to have
    the AuthenticationFault class. When we're running into this situation,
    we'll simply make sure we are in Desktop and if we are, we'll import
    the splash screen, let the user know an upgrade took place and reboot the
    app.
    """
    def _is_wrong_shotgun_api3_version(self):
        """
        Checks if we have the wrong version of shotgun_api3, ie
        AuthenticationFault doesn't exist.

        :returns True if we have the wrong version, False otherwise.
        """
        from tank_vendor import shotgun_api3
        return not hasattr(shotgun_api3, "AuthenticationFault")

    def _get_shotgun_desktop(self):
        """
        Returns the shotgun_desktop module, if available.

        :returns: The shotgun_desktop module or None.
        """
        try:
            import shotgun_desktop
            return shotgun_desktop
        except ImportError:
            return None

    def _reboot_app(self, shotgun_desktop):
        """
        Reboots the application. Calls sys.exit so this method never actually
        returns.

        :param shotgun_desktop: The shotgun_desktop module.
        """
        splash = shotgun_desktop.splash.Splash()
        splash.show()
        splash.raise_()
        splash.activateWindow()
        # Provide a countdown so the user knows that the desktop app is
        # being restarted on purpose because of a core update. Otherwise,
        # the user would get a flickering splash screen that from the user
        # point of view looks like the app is redoing work it already did by
        # mistake. This makes the behavior explicit.
        for i in range(3, 0, -1):
            splash.set_message(
                "Core updated. Restarting desktop in %d seconds..." % i)
            time.sleep(1)
        subprocess.Popen(sys.argv)
        sys.exit(0)

    def execute(self, *args, **kwargs):
        """
        Reboots the app if we have the wrong version of the Shotgun API and
        we're running the Shotgun Desktop.

        :raises Exception: Raised if we have then wrong version of Shotgun but are
            not running the Desktop. As of this writing, there's no reason for this
            to happen.
        """
        if self._is_wrong_shotgun_api3_version():
            shotgun_desktop = self._get_shotgun_desktop()
            if shotgun_desktop:
                self._reboot_app(shotgun_desktop)
            else:
                raise Exception(
                    "Wrong version of Shotgun API3. AuthenticationFault not accessible."
                )
コード例 #7
0
class FarmWrapper(sgtk.get_hook_baseclass()):

    # User setting used to track if a task will be publish locally
    # or on the farm.
    _SUBMIT_TO_FARM = "Submit to Farm"

    # local_property that will be used to keep track of who submitted
    # a job and will be used when registering a publish.
    _JOB_SUBMITTER = "job_submitter"

    @property
    def name(self):
        """
        :returns: Name of the plugin.
        """
        return self._SUBMIT_TO_FARM

    @property
    def settings(self):
        """
        Exposes the list of settings for this hook.

        :returns: Dictionary of settings definitions for the app.
        """
        # Inherit the settings from the base publish plugin
        base_settings = super(FarmWrapper, self).settings or {}

        # settings specific to this class
        submit_to_farm_settings = {
            self._SUBMIT_TO_FARM: {
                "type":
                "bool",
                "default":
                True,
                "description":
                "When set to True, this task will not be "
                "published inside the DCC and will be published "
                "on the render farm instead."
            }
        }

        # merge the settings
        base_settings.update(submit_to_farm_settings)
        return base_settings

    def create_settings_widget(self, parent):
        """
        Creates the widget for our plugin.

        :param parent: Parent widget for the settings widget.
        :type parent: :class:`QtGui.QWidget`

        :returns: Custom widget for this plugin.
        :rtype: :class:`QtGui.QWidget`
        """
        return FarmWrapperWidget(parent)

    def get_ui_settings(self, widget):
        """
        Retrieves the state of the ui and returns a settings dictionary.

        :param parent: The settings widget returned by :meth:`create_settings_widget`
        :type parent: :class:`QtGui.QWidget`

        :returns: Dictionary of settings.
        """
        return {self._SUBMIT_TO_FARM: widget.state}

    def set_ui_settings(self, widget, settings):
        """
        Populates the UI with the settings for the plugin.

        :param parent: The settings widget returned by :meth:`create_settings_widget`
        :type parent: :class:`QtGui.QWidget`
        :param list(dict) settings: List of settings dictionaries, one for each
            item in the publisher's selection.

        :raises NotImplementeError: Raised if this implementation does not
            support multi-selection.
        """
        if len(settings) > 1:
            raise NotImplementedError()
        settings = settings[0]
        widget.state = settings[self._SUBMIT_TO_FARM]

    def publish(self, settings, item):
        """
        Publishes a given task to Shotgun if it's the right time.

        :param dict settings: Dictionary of :class:`PluginSetting` object for this task.
        :param item: The item currently being published.
        :type item: :class:`PublishItem` to publish.
        """
        if self._is_submitting_to_farm(settings):
            # The publish_user will be picked up by the publish method at publishing time
            # on the render farm.
            item.local_properties.publish_user = sgtk.util.get_current_user(
                self.parent.sgtk)
            # We're inside the DCC and we're currently publishing a task
            # that will go on the farm, so we do nothing.
            self.logger.info("This publish will be submitted to the farm.")
        else:
            super(FarmWrapper, self).publish(settings, item)

    def finalize(self, settings, item):
        """
        Finalizes a given task if it's the right time.

        :param dict settings: Dictionary of :class:`PluginSetting` object for this task.
        :param item: The item currently being published.
        :type item: :class:`PublishItem` to publish.
        """
        if self._is_submitting_to_farm(settings):
            # We're inside the DCC and we're currently finalizing a task
            # that will go on the farm, so we do nothing.
            pass
        else:
            super(FarmWrapper, self).finalize(settings, item)

    def _is_submitting_to_farm(self, settings):
        """
        Indicates if we're currently submitting something to the farm.

        :param dict settings: Dictionary of :class:`PluginSetting` object for this task.

        :returns: ``True`` if the action should be taken, ``False`` otherwise.
        """
        # If the Submit to Farm setting is turned set and we're on the a user's machine
        if settings[self._SUBMIT_TO_FARM].value and _is_on_local_computer():
            # We are indeed submitting to the farm.
            return True
        else:
            # We're not currently submitting to the farm.
            return False
コード例 #8
0
class FarmSubmission(sgtk.get_hook_baseclass()):

    _SUBMIT_TO_FARM = "Submit to Farm"

    def post_publish(self, tree):
        """
        This hook method is invoked after the publishing phase.

        :param tree: The tree of items and tasks that has just been published.
        :type tree: :ref:`publish-api-tree`
        """
        if not _is_on_local_computer():
            return
        if not self._has_render_submissions(tree):
            self.logger.info("No job was submitted to the farm.")
            return

        # Grab some information about the context Toolkit is running in so
        # we can initialize Toolkit properly on the farm.
        engine = sgtk.platform.current_engine()
        dcc_state = {
            "session_path": _get_session_path(),
            "toolkit": {
                "pipeline_configuration_id": engine.sgtk.configuration_id,
                "context": engine.context.to_dict(),
                "engine_instance_name": engine.instance_name,
                "app_instance_name": self.parent.instance_name
            }
        }

        self._submit_to_farm(dcc_state, tree)
        self.logger.info("Job has been submitted to the render farm.")

    def _has_render_submissions(self, tree):
        """
        :returns: ``True`` if one task is submitting something to the farm, ``False`` otherwise.
        """
        for item in tree:
            for task in item.tasks:
                if self._SUBMIT_TO_FARM in task.settings and task.settings[
                        self._SUBMIT_TO_FARM].value:
                    return True
        return False

    def _submit_to_farm(self, dcc_state, tree):
        """
        Submits the job to the render farm.

        :param dict dcc_state: Information about the DCC and Toolkit.
        :param tree: The tree of items and tasks that has just been published.
        :type tree: :ref:`publish-api-tree`
        """
        # TODO: You are the render farm experts. We'll just mock the submission
        # here.

        submission_folder = "/var/tmp/webinar"
        if not os.path.exists(submission_folder):
            os.makedirs(submission_folder)

        tree.save_file(os.path.join(submission_folder, "publish2_tree.txt"))

        with open(os.path.join(submission_folder, "dcc_state.txt"), "wt") as f:
            # Make sure you call safe_dump, as accentuated characters
            # might not get encoded properly otherwise.
            yaml.safe_dump(dcc_state, f)

        self.logger.info(
            "Publishing context and state has been saved on disk for farm rendering.",
            extra={"action_show_folder": {
                "path": submission_folder
            }})
コード例 #9
0
class Bootstrap(get_hook_baseclass()):
    """
    Override the bootstrap core hook to cache some bundles ourselves.
    http://developer.shotgunsoftware.com/tk-core/core.html#bootstrap.Bootstrap
    """
    # List of github repos for which we download releases, with a github token to
    # do the download if the repo is private
    _download_release_from_github = [
        ("ue4plugins/tk-framework-unrealqt", ""),
        ("GPLgithub/tk-framework-unrealqt", ""),
    ]

    def can_cache_bundle(self, descriptor):
        """
        Indicates if a bundle can be cached by the :meth:`populate_bundle_cache_entry` method.

        This method is invoked when the bootstrap manager wants to cache a bundle used by a configuration.

        .. note:: This method is not called if the bundle is already cached so it
                  can't be used to update an existing cached bundle.

        :param descriptor: Descriptor of the bundle that needs to be cached.

        :returns: ``True`` if the bundle can be cached with this hook, ``False``
                  if not.
        :rtype: bool
        :raises RuntimeError: If six.moves is not available.
        """
        descd = descriptor.get_dict()
        # Some descriptors like shotgun descriptors don't have a path: ignore
        # them.
        if not descd.get("path"):
            return False
        return bool(self._should_download_release(descd["path"]))

    def populate_bundle_cache_entry(self, destination, descriptor, **kwargs):
        """
        Populates an entry from the bundle cache.

        This method will be invoked for every bundle for which :meth:`can_cache_bundle`
        returned ``True``. The hook is responsible for writing the bundle inside
        the destination folder. If an exception is raised by this method, the files
        will be deleted from disk and the bundle cache will be left intact.

        It has to properly copy all the files or the cache for this bundle
        will be left in an inconsistent state.

        :param str destination: Folder where the bundle needs to be written. Note
            that this is not the final destination folder inside the bundle cache.

        :param descriptor: Descriptor of the bundle that needs to be cached.
        """
        # This logic can be removed once we can assume tk-core is > v0.19.1 not
        # just in configs but also in the bundled Shotgun.app.
        try:
            from tank_vendor.six.moves.urllib import request as url2
            from tank_vendor.six.moves.urllib import error as error_url2
        except ImportError as e:
            self.logger.warning(_SIX_IMPORT_WARNING)
            self.logger.debug("%s" % e, exc_info=True)
            # Fallback on using urllib2
            import urllib2 as url2
            import urllib2 as error_url2

        descd = descriptor.get_dict()
        version = descriptor.version
        self.logger.info("Treating %s" % descd)
        # We check for the existence of the "path" key in can_cache_bundle
        # this method is only called if it exists.
        specs = self._should_download_release(descd["path"])
        if not specs:
            raise RuntimeError("Don't know how to download %s" % descd)
        name = specs[0]
        token = specs[1]
        try:
            if self.shotgun.config.proxy_handler:
                # Re-use proxy settings from the Shotgun connection
                opener = url2.build_opener(
                    self.parent.shotgun.config.proxy_handler, )
                url2.install_opener(opener)

            # Retrieve the release from the tag
            url = "https://api.github.com/repos/%s/releases/tags/%s" % (
                name, version)
            request = url2.Request(url)
            # Add the authorization token if we have one (private repos)
            if token:
                request.add_header("Authorization", "token %s" % token)
            request.add_header("Accept", "application/vnd.github.v3+json")
            try:
                response = url2.urlopen(request)
            except error_url2.URLError as e:
                if hasattr(e, "code"):
                    if e.code == 404:
                        self.logger.error("Release %s does not exists" %
                                          version)
                    elif e.code == 401:
                        self.logger.error(
                            "Not authorised to access release %s." % version)
                raise
            response_d = json.loads(response.read())
            # Look up for suitable assets for this platform. Assets names
            # follow this convention:
            #  <version>-py<python version>-<platform>.zip
            # We download and extract all assets for any Python version for
            # the current platform and version. We're assuming that the cached
            # config for a user will never be shared between machines with
            # different os.
            pname = {
                "Darwin": "osx",
                "Linux": "linux",
                "Windows": "win"
            }.get(platform.system())

            if not pname:
                raise ValueError("Unsupported platform %s" % platform.system())

            extracted = []
            for asset in response_d["assets"]:
                name = asset["name"]
                m = re.match("%s-py\d.\d-%s.zip" % (version, pname), name)
                if m:
                    # Download the asset payload
                    self._download_zip_github_asset(asset, destination, token)
                    extracted.append(asset)

            if not extracted:
                raise RuntimeError(
                    "Couldn't retrieve a suitable asset from %s" %
                    [a["name"] for a in response_d["assets"]])
            self.logger.info("Extracted files: %s from %s" %
                             (os.listdir(destination), ",".join(
                                 [a["name"] for a in extracted])))
        except Exception as e:
            # Log the exception with the backtrace because TK obfuscates it.
            self.logger.exception(e)
            raise

    def _should_download_release(self, desc_path):
        """
        Return a repo name and a token if the given descriptor path should be downloaded
        from a github release.

        :param str desc_path: A Toolkit descriptor path.
        :returns: A name, token tuple or ``None``.
        """
        for name, token in self._download_release_from_github:
            if "[email protected]:%s.git" % name == desc_path:
                return name, token
        return None

    def _download_zip_github_asset(self, asset, destination, token):
        """
        Download the zipped github asset and extract it into the given destination
        folder.

        Assets can be retrieved with the releases github REST api endpoint.
        https://developer.github.com/v3/repos/releases/#get-a-release-by-tag-name

        :param str asset: A Github asset dictionary.
        :param str destination: Full path to a folder where to extract the downloaded
                            zipped archive. The folder is created if it does not
                            exist.
        :param str token: A Github OAuth or personal token.
        """
        try:
            from tank_vendor.six.moves.urllib import request as url2
        except ImportError as e:
            self.logger.warning(_SIX_IMPORT_WARNING)
            self.logger.debug("%s" % e, exc_info=True)
            # Fallback on using urllib2
            import urllib2 as url2
        # If we have a token use a basic auth handler
        # just a http handler otherwise
        if token:
            passman = url2.HTTPPasswordMgrWithDefaultRealm()
            passman.add_password(None, asset["url"], token, token)
            auth_handler = url2.HTTPBasicAuthHandler(passman)
        else:
            auth_handler = url2.HTTPHandler()

        if self.shotgun.config.proxy_handler:
            # Re-use proxy settings from the Shotgun connection
            opener = url2.build_opener(
                self.parent.shotgun.config.proxy_handler, auth_handler)
        else:
            opener = url2.build_opener(auth_handler)

        url2.install_opener(opener)
        request = url2.Request(asset["url"])
        if token:
            # We will be redirected and the Auth shouldn't be in the header
            # for the redirection.
            request.add_unredirected_header("Authorization",
                                            "token %s" % token)
        request.add_header("Accept", "application/octet-stream")
        response = url2.urlopen(request)
        if not os.path.exists(destination):
            self.logger.info("Creating %s" % destination)
            os.makedirs(destination)
        tmp_file = os.path.join(destination, asset["name"])
        with open(tmp_file, "wb") as f:
            f.write(response.read())
        with zipfile.ZipFile(tmp_file, "r") as zip_ref:
            zip_ref.extractall(destination)
コード例 #10
0
class Bootstrap(get_hook_baseclass()):
    def init(self, shotgun, pipeline_configuration_id,
             configuration_descriptor, **kwargs):
        """
        Initializes the hook.

        This method is called right after the bootstrap manager reads this hook and passes in
        information about the pipeline configuration that will be used.

        The default implementation copies the arguments into the attributes named ``shotgun``,
        ``pipeline_configuration_id`` and ``configuration_descriptor``.

        :param shotgun: Connection to the Shotgun site.
        :type shotgun: :class:`~shotgun_api3.Shotgun`

        :param int pipeline_configuration_id: Id of the pipeline configuration we're bootstrapping into.
            If None, the ToolkitManager is bootstrapping into the base configuration.

        :param configuration_descriptor: Configuration the manager is bootstrapping into.
        :type configuration_descriptor: :class:`~sgtk.descriptor.ConfigDescriptor`
        """
        self.shotgun = shotgun
        self.pipeline_configuration_id = pipeline_configuration_id
        self.configuration_descriptor = configuration_descriptor

    def can_cache_bundle(self, descriptor):
        """
        Indicates if a bundle can be cached by the :meth:`populate_bundle_cache_entry` method.

        This method is invoked when the bootstrap manager wants to a bundle used by a configuration.

        The default implementation returns ``False``.

        :param descriptor: Descriptor of the bundle that needs to be cached.

        :returns: ``True`` if the bundle cache be cached with this hook, ``False``
                  if not.
        :rtype: bool
        """
        return False

    def populate_bundle_cache_entry(self, destination, descriptor, **kwargs):
        """
        Populates an entry from the bundle cache.

        This method will be invoked for every bundle for which :meth:`can_cache_bundle`
        returned ``True``. The hook is responsible for writing the bundle inside
        the destination folder. If an exception is raised by this method, the files
        will be deleted from disk and the bundle cache will be left intact.

        Be careful to properly copy all the files or the cache for this bundle
        will be left in an inconsistent state.

        The default implementation does nothing.

        :param str destination: Folder where the bundle needs to be written. Note
            that this is not the final destination folder inside the bundle cache.

        :param descriptor: Descriptor of the bundle that needs to be cached.
        """
        pass