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. """ item_settings = self._get_item_settings(settings) work_template = item.properties["work_template"] publish_template = item.properties["publish_template"] path = item_settings["to_publish"] # Check the filepath is valid if not work_template.validate(path): raise sgtk.TankError("The filepath '{}' does not match the template '{}'".format(path, work_template.name)) # Check it exists on disk glob_path = re.sub(r"\..+\.", ".*.", path) if not glob.glob(glob_path): raise sgtk.TankError("The filepath '{}' does not exist on disk".format(path)) fields = work_template.validate_and_get_fields(path) publish_path = publish_template.apply_fields(fields) item.properties["publish_path"] = publish_path item.properties["path"] = path image_seq = self._get_sequence_paths(item) if image_seq: item.properties["sequence_paths"] = image_seq return super(KatanaRenderPublishPlugin, self).validate(settings, item)
def compute_path(self, node): # Get relevant fields from the scene filename and contents work_file_fields = self.__get_hipfile_fields() if not work_file_fields: msg = "This Houdini file is not a Shotgun Toolkit work file!" raise sgtk.TankError(msg) # Get the templates from the app template = self._app.get_template("work_cache_template") # create fields dict with all the metadata fields = {} fields["name"] = work_file_fields.get("name") fields["version"] = work_file_fields["version"] fields["renderpass"] = node.name() fields["SEQ"] = "FORMAT: $F" # Get the camera width and height if necessary if "width" in template.keys or "height" in template.keys: # Get the camera cam_path = node.parm("geometry1_camera").eval() cam_node = hou.node(cam_path) if not cam_node: raise sgtk.TankError("Camera %s not found." % cam_path) fields["width"] = cam_node.parm("resx").eval() fields["height"] = cam_node.parm("resy").eval() fields.update(self._app.context.as_template_fields(template)) path = template.apply_fields(fields) path = path.replace(os.path.sep, "/") return path
def run(self): try: if sgtk.util.is_macos(): # use built-in screenshot command on the mac ret_code = os.system("screencapture -m -i -s %s" % self._path) if ret_code != 0: raise sgtk.TankError( "Screen capture tool returned error code %s" % ret_code ) elif sgtk.util.is_linux(): # use image magick ret_code = os.system("import %s" % self._path) if ret_code != 0: raise sgtk.TankError( "Screen capture tool returned error code %s. " "For screen capture to work on Linux, you need to have " "the imagemagick 'import' executable installed and " "in your PATH." % ret_code ) else: raise sgtk.TankError("Unsupported platform.") except Exception as e: self._error = str(e)
def compute_path(self, node): # Get relevant fields from the scene filename and contents work_file_fields = self.__get_hipfile_fields() if not work_file_fields: msg = "This Houdini file is not a Shotgun Toolkit work file!" raise sgtk.TankError(msg) # Get the templates from the app template = self._app.get_template("output_cache_template") # get the Step name field for the templated Mantra Output entity_name = "" asset_type = "" try: ctx = self._app.context entity_name = ctx.entity['name'] entity_type = ctx.entity['type'] except: msg = "Could not set the Shotgun context Entity name." self._app.log_debug(msg) raise sgtk.TankError(msg) # create fields dict with all the metadata fields = {} if entity_type == "Shot": fields = { "name": work_file_fields.get("name", None), "node": node.name(), "renderpass": node.name(), "HSEQ": "FORMAT: $F", "version": work_file_fields.get("version", None), "Shot": entity_name, "Step": work_file_fields.get("Step", None) } # Asset Template fields if entity_type == "Asset": # Set the Custom Asset Type asset_type = work_file_fields.get("sg_asset_type", None) fields = { "name": work_file_fields.get("name", None), "node": node.name(), "renderpass": node.name(), "HSEQ": "FORMAT: $F", "version": work_file_fields.get("version", None), "Asset": entity_name, "sg_asset_type": asset_type, "Step": work_file_fields.get("Step", None) } fields.update(self._app.context.as_template_fields(template)) path = template.apply_fields(fields) path = path.replace(os.path.sep, "/") return path
def __install_extension(ext_path, dest_dir, logger): """ Installs the supplied extension path by unzipping it directly into the supplied destination directory. :param ext_path: The path to the .zxp extension. :param dest_dir: The CEP extension's destination :return: """ # move the installed extension to the backup directory if os.path.exists(dest_dir): backup_ext_dir = tempfile.mkdtemp() logger.debug("Backing up the installed extension to: %s" % (backup_ext_dir, )) try: backup_folder(dest_dir, backup_ext_dir) except Exception: shutil.rmtree(backup_ext_dir) raise sgtk.TankError( "Unable to create backup during extension update.") # now remove the installed extension logger.debug("Removing the installed extension directory...") try: shutil.rmtree(dest_dir) except Exception: # try to restore the backup move_folder(backup_ext_dir, dest_dir) raise sgtk.TankError( "Unable to remove the old extension during update.") logger.debug("Installing bundled extension: '%s' to '%s'" % (ext_path, dest_dir)) # make sure the bundled extension exists if not os.path.exists(ext_path): # retrieve backup before aborting install move_folder(backup_ext_dir, dest_dir) raise sgtk.TankError( "Expected CEP extension does not exist. Looking for %s" % (ext_path, )) # extract the .zxp file into the destination dir with contextlib.closing(zipfile.ZipFile(ext_path, "r")) as ext_zxp: ext_zxp.extractall(dest_dir) # if we're here, the install was successful. remove the backup try: logger.debug("Install success. Removing the backed up extension.") shutil.rmtree(backup_ext_dir) except Exception: # can't remove temp dir. no biggie. pass
def _external_screenshot(): """ Use an external approach for grabbing a screenshot. Linux and macosx support only. :returns: Captured image :rtype: :class:`~PySide.QtGui.QPixmap` """ output_path = tempfile.NamedTemporaryFile(suffix=".png", prefix="screencapture_", delete=False).name pm = None try: # do screenshot with thread so we don't block anything screenshot_thread = ExternalCaptureThread(output_path) screenshot_thread.start() while not screenshot_thread.isFinished(): screenshot_thread.wait(100) QtGui.QApplication.processEvents() if screenshot_thread.error_message: raise sgtk.TankError("Failed to capture " "screenshot: %s" % screenshot_thread.error_message) # load into pixmap: pm = QtGui.QPixmap(output_path) finally: # remove the temporary file if output_path and os.path.exists(output_path): os.remove(output_path) return pm
def _get_render_resolution(node): """Returns render resolution for supplied node based on its camera parm. :param hou.Node node: The node being acted upon. """ # Get the camera cam_path = node.parm("camera").eval() cam_node = hou.node(cam_path) if not cam_node: raise sgtk.TankError("Camera %s not found." % (cam_path, )) width = cam_node.parm("resx").eval() height = cam_node.parm("resy").eval() # Calculate Resolution Override if node.parm("override_camerares").eval(): scale = node.parm("res_fraction").eval() if scale == "specific": width = node.parm("res_overridex").eval() height = node.parm("res_overridey").eval() else: width = int(float(width) * float(scale)) height = int(float(height) * float(scale)) return width, height
def __getattr__(self, name): raise sgtk.TankError( "Looks like you are trying to run an App that uses a QT " "based UI, however the Shell engine could not find a PyQt " "or PySide installation in your python system path. We " "recommend that you install PySide if you want to " "run UI applications from the Shell.")
def _compute_backup_output_path(self, node): # get relevant fields from the current file path work_file_fields = self._get_hipfile_fields() if not work_file_fields: msg = "This Houdini file is not a Shotgun Toolkit work file!" raise sgtk.TankError(msg) # Get the type of output type_parm = node.parm('types') extension = type_parm.menuLabels()[type_parm.evalAsInt()] # create fields dict with all the metadata fields = { "name": work_file_fields.get("name", None), "node": self._getNodeName(node), "version": node.parm('ver').evalAsInt(), "ext": extension, } output_profile = self._get_output_profile(node) output_cache_template = self._app.get_template_by_name( output_profile["output_backup_template"]) fields.update( self._app.context.as_template_fields(output_cache_template)) path = output_cache_template.apply_fields(fields) path = path.replace(os.path.sep, "/") return path
def _compute_backup_output_path(self, node): # get relevant fields from the current file path work_file_fields = self._get_hipfile_fields() if not work_file_fields: msg = "This Houdini file is not a Shotgun Toolkit work file!" raise sgtk.TankError(msg) output_profile = self._get_output_profile(node) output_cache_template = self._app.get_template_by_name( output_profile["output_backup_render_template"]) # create fields dict with all the metadata fields = { "name": work_file_fields.get("name", None), "RenderLayer": node.name().replace('_', ''), "version": node.parm('ver').evalAsInt(), "Camera": node.parm("camera").evalAsString().split('/')[-1].replace('_', ''), } fields.update(self._app.context.as_template_fields( output_cache_template)) path = output_cache_template.apply_fields(fields) path = path.replace(os.path.sep, "/") return path
def test_download_local(self): """ Test that download_local downloads from the correct URL, and handles an Exception as expected. """ desc = self._create_desc() expected_url = "https://github.com/{o}/{r}/archive/{v}.zip" expected_url = expected_url.format( o=self.default_location_dict["organization"], r=self.default_location_dict["repository"], v=self.default_location_dict["version"], ) with patch("tank.util.shotgun.download.download_and_unpack_url" ) as download_and_unpack_url_mock: # Raise an exception first and ensure it's caught and a TankDescriptorError is raised. download_and_unpack_url_mock.side_effect = sgtk.TankError() with self.assertRaises(sgtk.descriptor.TankDescriptorError): desc.download_local() with patch("tank.util.shotgun.download.download_and_unpack_url" ) as download_and_unpack_url_mock: # Now reset and let the download "succeed" and ensure the correct calls were made, and # the expected arguments were passed. desc.download_local() calls = download_and_unpack_url_mock.call_args_list self.assertEqual(len(calls), 1) # calls will be a tuple of call objects, which can be indexed into to # get tuples of (args, kwargs). # first positional arg of first call self.assertEqual(calls[0][0][0], self.mockgun) # second positional arg of first call self.assertEqual(calls[0][0][1], expected_url)
def search(self, text): """ Triggers the popup to display results based on the supplied text. :param text: current contents of editor """ if len(text) < self.COMPLETE_MINIMUM_CHARACTERS: # global search wont work with shorter than 3 len strings # for these cases, clear the auto completer model fully # there is no more work to do self.clear() return # now we are about to populate the model with data # and therefore trigger the completer to pop up. # # The completer seems to have some internal properties # which are transitory and won't last between sessions. # for these, we have to set them up every time the # completion process is about to start it seems. # tell completer to render matches using our delegate # configure how the popup should look self._set_item_delegate(self.popup(), text) # try to disconnect and reconnect the activated signal # it seems this signal is lost every time the widget # looses focus. try: self.activated[QtCore.QModelIndex].disconnect(self._on_select) except Exception: self._bundle.log_debug( "Could not disconnect activated signal prior to " "reconnect. Looks like this connection must have been " "discarded at some point along the way." ) self.activated[QtCore.QModelIndex].connect(self._on_select) # now clear the model self._clear_model() # clear thumbnail map self._thumb_map = {} # kick off async data request from shotgun # we request to run an arbitrary method in the worker thread # this _do_sg_global_search method will be called by the worker # thread when the worker queue reaches that point and will # call out to execute it. The data dictionary specified will # be forwarded to the method. if self._sg_data_retriever: # clear download queue and do the new search self._sg_data_retriever.clear() self._processing_id = self._launch_sg_search(text) else: raise sgtk.TankError( "Please associate this class with a background task manager." )
def _compute_output_path(self, node): # get relevant fields from the current file path work_file_fields = self._get_hipfile_fields() if not work_file_fields: msg = "This Houdini file is not a Shotgun Toolkit work file!" raise sgtk.TankError(msg) output_profile = self._get_output_profile(node) # Get the cache templates from the app output_cache_template = self._app.get_template_by_name( output_profile["output_cache_template"]) # create fields dict with all the metadata fields = { "name": work_file_fields.get("name", None), "node": node.name(), "renderpass": node.name(), "SEQ": "FORMAT: $F", "version": work_file_fields.get("version", None), } fields.update( self._app.context.as_template_fields(output_cache_template)) path = output_cache_template.apply_fields(fields) path = path.replace(os.path.sep, "/") return path
def ensure_extension_up_to_date(logger): """ Carry out the necessary operations needed in order for the Adobe extension to be recognized. This inlcudes copying the extension from the engine location to a OS-specific location. """ # the basic plugin needs to be installed in order to launch the Adobe # engine. we need to make sure the plugin is installed and up-to-date. # will only run if SHOTGUN_ADOBE_DISABLE_AUTO_INSTALL is not set. if "SHOTGUN_ADOBE_DISABLE_AUTO_INSTALL" not in os.environ: logger.debug("Ensuring Adobe extension is up-to-date...") try: __ensure_extension_up_to_date(logger) except Exception: exc = traceback.format_exc() raise sgtk.TankError( "There was a problem ensuring the Adobe integration extension " "was up-to-date with your toolkit engine. If this is a " "recurring issue please contact us via %s. " "The specific error message encountered was:\n'%s'." % ( sgtk.support_url, exc, ))
def __getattr__(self, name): raise sgtk.TankError( "Looks like you are trying to run an App that uses a QT based UI, however the " "python installation that the Desktop engine is currently using does not seem " "to contain a valid PySide or PyQt4 install. Either install PySide into your " "python environment or alternatively switch back to using the native Shotgun " "Desktop python installation, which includes full QT support." )
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. """ item_settings = self._get_item_settings(settings) # Check the templates exist work_template = item.properties["work_template"] if not work_template: raise sgtk.TankError( "'{}': Work template '{}' doesn't exist".format( item.name, work_template.name)) publish_template = item.properties["publish_template"] if not publish_template: raise sgtk.TankError( "'{}': Publish template '{}' doesn't exist".format( item.name, publish_template.name)) # Check the file exists still. It might have been removed from the filesystem after collection path = item_settings["to_publish"] if not os.path.exists(path): raise sgtk.TankError( "The file '{}' no longer exists on disk. Has it been deleted/published?" .format(path)) # Check the filepath is valid if not work_template.validate(path): raise sgtk.TankError( "The filepath '{}' does not match the template '{}'".format( path, work_template.name)) # Check if file has already been copied to the publish location fields = work_template.validate_and_get_fields(path) publish_path = publish_template.apply_fields(fields) item.properties["publish_path"] = publish_path item.properties["path"] = path if os.path.exists(publish_path): raise IOError( errno.EEXIST, "The file '{}' has already been copied to the publish location." .format(path)) return True
def value(self, value): """ Set the widget's value. :param value: The value to set the widget's value to. :type value: any """ raise sgtk.TankError("Abstract class method not overriden")
def restore(self, state): """ Restore the widget to the provided state. :param state: The state to restore the widget from. :type state: any """ raise sgtk.TankError("Abstract class method not overriden")
def init_engine(self): self.logger.debug("%s: Initializing..." % self) if self.context.project is None: # must have at least a project in the context to even start! raise sgtk.TankError("The Motionbuilder engine needs at least a project in the context " "in order to start! Your context: %s" % self.context) # motionbuilder doesn't have good exception handling, so install our own trap sys.excepthook = sgtk_mobu_exception_trap
def persistent(self, is_persistent): """Set the item to persistent or not. Only top-level items can be set to persistent. """ # It's not a crime to turn persistence off, so raise an error when # actually trying it on an invalid item. if is_persistent and (not self.parent or not self.parent.is_root): raise sgtk.TankError( "Only top-level tree items can be made persistent.") self._persistent = is_persistent
def remove_item(self, child_item): """ Remove the supplied child :ref:`publish-api-item` of this item. :param child_item: The child :ref:`publish-api-item` to remove. """ if child_item not in self.children: raise sgtk.TankError( "Unable to remove child item. Item %s is not a child of %s in " "the publish tree." % (child_item, self)) self._children.remove(child_item)
def _get_template(self, template_name): """ Get a shotgun template from the given name. :param str template_name: The name of the template to get. :rtype: An :class:`sgtk.Template` instance. :raises: :class:`sgtk.TankError` if no template name supplied or the template doesn't exist. """ setting_template_name = self.extra_args.get(template_name) if not setting_template_name: raise sgtk.TankError( "No template name '{}' defined for node type '{}'" "".format(template_name, self.NODE_TYPE)) template = self.parent.get_template_by_name(setting_template_name) if not template: raise sgtk.TankError( "Can't find template called '{}' defined for node type '{}'" "".format(setting_template_name, self.NODE_TYPE)) return template
def __compute_path(self, node, settings, template_alias, aov_name=None): # Get relevant fields from the scene filename and contents work_file_fields = self.__get_hipfile_fields() if not work_file_fields: msg = "This Houdini file is not a Shotgun Toolkit work file!" raise sgtk.TankError(msg) # Get the templates from the node settings template_name = settings.get(template_alias) template = self._app.get_template_by_name(template_name) if not template: msg = 'No Template provided for "{0}"' raise sgtk.TankError(msg.format(template_name)) # create fields dict with all the metadata fields = dict() fields["name"] = work_file_fields.get("name") fields["version"] = work_file_fields["version"] # fields["node"] = self.get_node_name(node) fields["renderpass"] = self.get_node_name(node) fields["SEQ"] = "FORMAT: $F" if aov_name: # fields["channel"] = channel_name fields["aov_name"] = aov_name # Get the camera width and height if necessary if "width" in template.keys or "height" in template.keys: width, height = self.__gather_render_resolution(node) fields["width"] = width fields["height"] = height fields.update(self._app.context.as_template_fields(template)) path = template.apply_fields(fields) path = path.replace('\\', '/') return path
def init_engine(self): """ Main initialization entry point. """ self.logger.debug("%s: Initializing..." % self) if hou.applicationVersion()[0] < 14: raise sgtk.TankError( "Your version of Houdini is not supported. Currently, Toolkit " "only supports version 14+.") # keep track of if a UI exists self._ui_enabled = hasattr(hou, 'ui')
def _compute_output_path(self, node): # get relevant fields from the current file path work_file_fields = self._get_hipfile_fields(node) if not work_file_fields: msg = "This Houdini file is not a Shotgun Toolkit work file!" raise sgtk.TankError(msg) output_profile = self._get_output_profile(node) if node.evalParm('shared_out'): output_cache_template = self._app.get_template_by_name( output_profile["output_cache_shared_template"]) else: # Get the cache templates from the app output_cache_template = self._app.get_template_by_name( output_profile["output_cache_template"]) # Get the type of output extension = node.evalParm('types') types = { 0: 'bgeo.sc', 1: 'obj', 2: 'bgeo', 3: 'vdb', } # create fields dict with all the metadata fields = { "name": work_file_fields.get("name", None), "node": node.name(), "renderpass": node.name(), "version": work_file_fields.get("version", None), "geo.ext": types[extension] } if node.evalParm('seq') == 1: fields["SEQ"] = "FORMAT: $F" else: fields["SEQ"] = None fields.update( self._app.context.as_template_fields(output_cache_template)) path = output_cache_template.apply_fields(fields) path = path.replace(os.path.sep, "/") return path
def _compute_output_path(self, node, template_name, aov_name=None): """Compute output path based on current work file and render template. :param hou.Node node: The node being acted upon. :param str template_name: The name of template to compute a path for. :param str aov_name: Optional AOV name used to compute the path. """ # Get relevant fields from the scene filename and contents work_file_fields = self._get_hipfile_fields() if not work_file_fields: msg = "This Houdini file is not a Shotgun Toolkit work file!" raise sgtk.TankError(msg) output_profile = self._get_output_profile(node) # Get the render template from the app output_template = self._app.get_template_by_name( output_profile[template_name]) # create fields dict with all the metadata fields = { "name": work_file_fields.get("name", None), "node": node.name(), "renderpass": node.name(), "SEQ": "FORMAT: $F", "version": work_file_fields.get("version", None), } # use %V - full view printout as default for the eye field fields["eye"] = "%V" if aov_name: fields["aov_name"] = aov_name # Get the camera width and height if necessary if "width" in output_template.keys or "height" in output_template.keys: width, height = _get_render_resolution(node) fields["width"] = width fields["height"] = height fields.update(self._app.context.as_template_fields(output_template)) path = output_template.apply_fields(fields) path = path.replace(os.path.sep, "/") return path
def capture_screenshot(self, path): if sys.platform == "linux2": # use image magick in sgtk clean env context = self.parent.context print context ret_code, error_msg = dd_jstools_utils.run_in_clean_env( ["import", path], context) if ret_code != 0: raise sgtk.TankError( "Screen capture tool returned error code: %s, message: `%s`." "For screen capture to work on Linux, you need to have " "the imagemagick 'import' executable installed and " "in your PATH." % (ret_code, error_msg)) else: super(ExternalScreenshot, self).capture_screenshot(path)
def create_new_task(self, name, pipeline_step, entity, assigned_to=None): """ Create a new task with the specified information. :param name: Name of the task to be created. :param pipeline_step: Pipeline step associated with the task. :type pipeline_step: dictionary with keys 'Type' and 'id' :param entity: Entity associated with this task. :type entity: dictionary with keys 'Type' and 'id' :param assigned_to: User assigned to the task. Can be None. :type assigned_to: dictionary with keys 'Type' and 'id' :returns: The created task. :rtype: dictionary with keys 'step', 'project', 'entity', 'content' and 'task_assignees' if 'assigned_to' was set. :raises sgtk.TankError: On error, during validation or creation, this method raises a TankError. """ app = self.parent # construct the data for the new Task entity: data = { "step": pipeline_step, "project": app.context.project, "entity": entity, "content": name, } if assigned_to: data["task_assignees"] = [assigned_to] # create the task: sg_result = app.shotgun.create("Task", data) if not sg_result: raise sgtk.TankError("Failed to create new task - reason unknown!") # try to set it to IP - not all studios use IP so be careful! try: app.shotgun.update("Task", sg_result["id"], {"sg_status_list": "ip"}) except: pass return sg_result
def take_over(func): """ Decorator to accumulate the names of members to take over in derived classes. :param func: A function or method to takeover in the derived class. """ if hasattr(func, "__name__"): # regular method name = func.__name__ elif hasattr(func, "__func__"): # static or class method name = func.__func__.__name__ else: raise sgtk.TankError("Don't know how to take over member: %s" % (func,)) TAKE_OVER_NAMES.append(name) return func
def _compute_output_path(self, node, template_name, aov_name=None): """Compute output path based on current work file and render template. :param hou.Node node: The node being acted upon. :param str template_name: The name of template to compute a path for. :param str aov_name: Optional AOV name used to compute the path. """ # Get relevant fields from the scene filename and contents work_file_fields = self._get_hipfile_fields() if not work_file_fields: msg = "This Houdini file is not a Shotgun Toolkit work file!" raise sgtk.TankError(msg) output_profile = self._get_output_profile(node) # Get the render template from the app output_template = self._app.get_template_by_name( output_profile[template_name]) # create fields dict with all the metadata fields = { "name": work_file_fields.get("name", None), "Step": work_file_fields.get("Step", None), "RenderLayer": node.name().replace('_', ''), "Camera": node.parm("camera").evalAsString().split('/')[-1].replace('_', ''), "SEQ": "FORMAT: $F", "version": node.parm('ver').evalAsInt(), "AOV": self.TK_DEFAULT_AOV } # use %V - full view printout as default for the eye field fields["eye"] = "%V" if aov_name: fields["aov_name"] = aov_name fields.update(self._app.context.as_template_fields(output_template)) path = output_template.apply_fields(fields) path = path.replace(os.path.sep, "/") return path