# # 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.
# 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.
# 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
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)
# 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)
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." )
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
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 }})
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)
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