def create_mipmaps_for_seq(self, source_paths, target_seq_path): """ Given a list of files, convert each to a mipmap file and save to target path. :param source_paths: list of source image files :param target_seq_path: path (with an seq field if source is an image sequence) where the mipmaps should be written :returns: list of created mipmap paths """ publisher = self.parent mipmap_paths = [] for source_path in source_paths: frame = publisher.util.get_frame_number(source_path) if frame: target_path = publisher.util.get_path_for_frame( target_seq_path, frame) if not target_path: # We do not want the conversion of multiple images to overwrite # a single target path. Something is wrong with the configuration. raise TankError("Source path: {} contains a frame number, " "but target path: {} does not.".format( source_path, target_path)) else: target_path = target_seq_path if not self._create_mipmap(source_path, target_path): self.logger.warning("Mipmap not created for: {}. " "Touching empty file.".format(target_path)) open(target_path, 'a').close() else: mipmap_paths.append(target_path) return mipmap_paths
def __get_tk_path_from_flame_plate_path(self, flame_path): """ Given a xxx.[1234-1234].exr style Flame plate path, return the equivalent, normalized tk path, e.g. xxx.%04d.exr :param flame_path: Flame style plate path (must match the plate template) :returns: tk equivalent """ template = self._app.sgtk.template_from_path(flame_path) if template is None: # the path does not match any template. This shouldn't happen since these # paths were all generated by the shotgun integration, however is possible because # of some known bugs in flame, where updated paths returned by flame hooks are not being # used by the flame system. A typical example is when a sequence name contains a space or # other special character - the toolkit template system will adjust the path to replace # spaces with underscores. These adjusted paths are returned to Flame but are not picked # up, resulting in the paths returned here not actually being valid. raise TankError( "The path '%s' does not match any template in the Toolkit configuration. " "This sometimes happens if Flame sequences or clips contain special characters " "such as slashes or spaces." % flame_path) fields = template.get_fields(flame_path) fields["SEQ"] = "FORMAT: %d" fields["flame.frame"] = "FORMAT: %d" return template.apply_fields(fields)
def _resolve_work_path_template(self, properties, path): """ Resolve work_path_template from the properties. The signature uses properties, so that it can resolve the template even if the item object hasn't been created. :param properties: properties that have/will be used to build item object. :param path: path to be used to get the templates, using template_from_path. :return: Name of the template. """ work_path_template = properties.get("work_path_template") # If defined, add the work_path_template to the item's properties if work_path_template: work_tmpl = self.parent.get_template_by_name(work_path_template) if not work_tmpl: # this template was not found in the template config! raise TankError("The template '%s' does not exist!" % work_path_template) # Else see if the path matches an existing template else: # let's try to check if this path fits into any known template work_tmpl = self.sgtk.template_from_path(path) if not work_tmpl: # this path doesn't map to any known templates! self.logger.warning( "Cannot find a matching template for path: %s" % path) else: # update the field with correct value so that we can use it everytime for this item work_path_template = work_tmpl.name return work_path_template
def get_publish_template(self, settings, item): """ Retrieves and and validates the publish template from the 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 :returns: A template representing the publish path of the item or None if no template could be identified. """ publisher = self.parent publish_template = item.get_property("publish_template") if publish_template: return publish_template publish_template = None publish_template_setting = settings.get("Publish Template") if publish_template_setting and publish_template_setting.value: publish_template = publisher.engine.get_template_by_name( publish_template_setting.value) if not publish_template: raise TankError( "Missing Publish Template in templates.yml: %s " % publish_template_setting.value) # cache it for later use item.properties["publish_template"] = publish_template return publish_template
def execute(self, path, context): """ Bootstrap launches VRED Presenter :param path: full path to the published file :param context: context object representing the publish """ tk = self.parent.sgtk software_launcher = sgtk.platform.create_engine_launcher(tk, context, "tk-vred") software_versions = software_launcher.scan_for_presenter() presenter_versions = [] for version in software_versions: if re.search("Presenter", version.product): presenter_versions.append(version) presenter_version = presenter_versions[-1] launch_info = software_launcher.prepare_launch(presenter_version.path, "") env = os.environ.copy() for k in launch_info.environment: if k == "SGTK_CONTEXT": env[k] = sgtk.context.serialize(context) else: env[k] = str(launch_info.environment[k]) try: launched = subprocess.Popen( [launch_info.path, launch_info.args, path], env=env ) except RuntimeError: raise TankError( "Unable to launch VRED Presenter in context " "%r for file %s." % (context, path) ) if launched: return True else: return False
def get_layer_name_template(self, settings, item): """ Retrieves and and validates the Layer Name Template, used to build a name for the individual layers that will be published. :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: A template representing the Layer Name template of the item or None if no template could be identified. """ publisher = self.parent layer_name_template = item.get_property("layer_name_template") if layer_name_template: return layer_name_template layer_name_template = None layer_name_template_setting = settings.get("Layer Name Template") if layer_name_template_setting and layer_name_template_setting.value: layer_name_template = publisher.engine.get_template_by_name( layer_name_template_setting.value) if not layer_name_template: raise TankError( "Missing Layer Name Template in templates.yml: %s " % layer_name_template_setting.value) # cache it for later use item.properties["layer_name_template"] = layer_name_template return layer_name_template
def get_dcc_versions(self, env, fields, work_template, publish_template): # version is used so we need to find the latest version - this means # searching for files... # need a file key to find all versions so lets build it: file_versions = None file_key = FileItem.build_file_key(fields, work_template, env.version_compare_ignore_fields) key = str(env) + str(fields) + str(work_template) + str(publish_template) if self._file_model: cached = self._file_model._search_cache._cache.get(key) if cached: file_versions = cached # file_versions = self._file_model.get_cached_file_versions(file_key, env, clean_only=True) if file_versions is None: # fall back to finding the files manually - this will be slower! try: finder = FileFinder() files = finder.find_files(work_template, publish_template, env.context, file_key) or [] except TankError, e: raise TankError("Failed to find files for this work area: %s" % e) file_versions = [f.version for f in files] self._file_model._search_cache._cache[key] = file_versions
def _on_browse_for_publishes(self, new_project_form): """ Called when the user clicks the 'Add Publishes' button in the new project form. Opens the loader so that the user can select a publish to be loaded into the new project. :param new_project_form: The new project form that the button was clicked in """ loader_app = self._app.engine.apps.get("tk-multi-loader2") if not loader_app: raise TankError( "The tk-multi-loader2 app needs to be available to browse for publishes!" ) # browse for publishes: publish_types = self._app.get_setting("publish_types") selected_publishes = loader_app.open_publish( "Select Published Geometry", "Select", publish_types) # make sure we keep this list of publishes unique: current_ids = set([p["id"] for p in self.__new_project_publishes]) for sg_publish in selected_publishes: publish_id = sg_publish.get("id") if publish_id != None and publish_id not in current_ids: current_ids.add(publish_id) self.__new_project_publishes.append(sg_publish) # update new project form with selected geometry: new_project_form.update_publishes(self.__new_project_publishes)
def add_task( self, cbl, priority=None, group=None, upstream_task_ids=None, task_args=None, task_kwargs=None, ): """ Add a new task to the queue. A task is a callable method/class together with any arguments that should be passed to the callable when it is called. :param cbl: The callable function/class to call when executing the task :param priority: The priority this task should be run with. Tasks with higher priority are run first. :param group: The group this task belongs to. Task groups can be used to simplify task management (e.g. stop a whole group, be notified when a group is complete) :param upstream_task_ids: A list of any upstream tasks that should be completed before this task is run. The results from any upstream tasks are appended to the kwargs for this task. :param task_args: A list of unnamed parameters to be passed to the callable when running the task :param task_kwargs: A dictionary of named parameters to be passed to the callable when running the task :returns: A unique id representing the task. """ if not callable(cbl): raise TankError( "The task function, method or object '%s' must be callable!" % cbl ) upstream_task_ids = set(upstream_task_ids or []) # create a new task instance: task_id = self._next_task_id self._next_task_id += 1 new_task = BackgroundTask(task_id, cbl, group, priority, task_args, task_kwargs) # add the task to the pending queue: # If priority is None, then use 0 so when we sort we're only comparing integers. # Python 3 raises an error when comparing int with NoneType. self._pending_tasks_by_priority.setdefault(priority or 0, []).append(new_task) # add tasks to various look-ups: self._tasks_by_id[new_task.uid] = new_task self._group_task_map.setdefault(group, set()).add(new_task.uid) # keep track of the task dependencies: self._upstream_task_map[new_task.uid] = upstream_task_ids for us_task_id in upstream_task_ids: self._downstream_task_map.setdefault(us_task_id, set()).add(new_task.uid) self._low_level_debug_log("Added Task %s to the queue" % new_task) # and start the next task: self._start_tasks() return new_task.uid
def find_change_containing(p4, path): """ Find the current change that the specified path is in. """ try: p4_res = p4.run_fstat(path) except P4Exception, e: raise TankError("Perforce: %s" % (p4.errors[0] if p4.errors else e))
def _update_item_thumbnail(self, pixmap): """ Update the currently selected item with the given thumbnail pixmap """ if not self._current_item: raise TankError("No current item set!") self._current_item.thumbnail = pixmap
def batch_version_number(self): """ Return the version number associated with the batch file """ if not self.has_batch_export: raise TankError("Cannot get batch path - no batch metadata found!") return int(self._flame_batch_data["versionNumber"])
def _get_hook_value(self, method_name, hook_key): """ Validate that value is correct and return it """ if method_name not in self._hook_data: raise TankError("Unknown shotgun_fields hook method %s" % method_name) data = self._hook_data[method_name] if hook_key not in data: raise TankError("Hook shotgun_fields.%s does not return " "required dictionary key '%s'!" % (method_name, hook_key)) return data[hook_key]
def _get_entity(self): """ Returns the most relevant playback entity (as a sg std dict) for the current context """ # figure out the context for Screening Room # first try to get a version # if that fails try to get the current entity rv_context = None task = self.context.task if task: # look for versions matching this task! self.logger.debug("Looking for versions connected to %s..." % task) filters = [["sg_task", "is", task]] order = [{"field_name": "created_at", "direction": "desc"}] fields = ["id"] version = self.shotgun.find_one("Version", filters=filters, fields=fields, order=order) if version: # got a version rv_context = version if rv_context is None and self.context.entity: # fall back on entity # try to extract a version (because versions are launched in a really nice way # in Screening Room, while entities are not so nice...) self.logger.debug("Looking for versions connected to %s..." % self.context.entity) filters = [["entity", "is", self.context.entity]] order = [{"field_name": "created_at", "direction": "desc"}] fields = ["id"] version = self.shotgun.find_one("Version", filters=filters, fields=fields, order=order) if version: # got a version rv_context = version else: # no versions, fall back onto just the entity rv_context = self.context.entity if rv_context is None: # fall back on project rv_context = self.context.project if rv_context is None: raise TankError( "Not able to figure out a current context to launch screening room for!" ) self.logger.debug("Closest match to current context is %s" % rv_context) return rv_context
def submit_change(p4, change): """ Submit the specified change """ try: change_spec = p4.fetch_change("-o", str(change)) p4.run_submit(change_spec) except P4Exception, e: raise TankError("Perforce: %s" % (p4.errors[0] if p4.errors else e))
def shotgun_version_id(self): """ Returns the ShotGrid id for the version associated with this segment, if there is one. """ if not self.has_shotgun_version: raise TankError( "Cannot get ShotGrid version id for segment - no version associated!" ) return self._shotgun_version_id
def batch_path(self): """ Return the flame batch export path for this shot """ if not self.has_batch_export: raise TankError("Cannot get batch path - no batch metadata found!") return os.path.join(self._flame_batch_data.get("destinationPath"), self._flame_batch_data.get("resolvedPath"))
def _get_active_document(self): """ Returns the currently open document in Photoshop. Raises an exeption if no document is active. """ doc = photoshop.app.activeDocument if doc is None: raise TankError("There is no currently active document!") return doc
def pre_app_init(self): """ Engine construction/setup done before any apps are initialized """ self.log_debug("%s: Initializing..." % self) # check that this version of Mari is supported: MIN_VERSION = (2, 6, 1) # completely unsupported below this! MAX_VERSION = (3, 0) # untested above this so display a warning mari_version = mari.app.version() if (mari_version.major() < MIN_VERSION[0] or (mari_version.major() == MIN_VERSION[0] and mari_version.minor() < MIN_VERSION[1])): # this is a completely unsupported version of Mari! raise TankError( "This version of Mari (%d.%dv%d) is not supported by Shotgun Toolkit. The" "minimum required version is %d.%dv%d." % (mari_version.major(), mari_version.minor(), mari_version.revision(), MIN_VERSION[0], MIN_VERSION[1], MIN_VERSION[2])) elif (mari_version.major() > MAX_VERSION[0] or (mari_version.major() == MAX_VERSION[0] and mari_version.minor() > MAX_VERSION[1])): # this is an untested version of Mari msg = ( "The Shotgun Pipeline Toolkit has not yet been fully tested with Mari %d.%dv%d. " "You can continue to use the Toolkit but you may experience bugs or " "instability. Please report any issues you see to [email protected]" % (mari_version.major(), mari_version.minor(), mari_version.revision())) if (self.has_ui and "SGTK_MARI_VERSION_WARNING_SHOWN" not in os.environ and mari_version.major() >= self.get_setting("compatibility_dialog_min_version")): # show the warning dialog the first time: mari.utils.message(msg, "Shotgun") os.environ["SGTK_MARI_VERSION_WARNING_SHOWN"] = "1" self.log_warning(msg) try: self.log_user_attribute_metric( "Mari version", "%s.%s.%s" % (mari_version.major(), mari_version.minor(), mari_version.revision())) except: # ignore all errors. ex: using a core that doesn't support metrics pass # cache handles to the various manager instances: tk_mari = self.import_module("tk_mari") self.__geometry_mgr = tk_mari.GeometryManager() self.__project_mgr = tk_mari.ProjectManager() self.__metadata_mgr = tk_mari.MetadataManager()
class ProjectManager(object): """ Handle all Mari project management """ def __init__(self, app): """ Construction :param app: The Application instance that created this instance """ self._app = app self.__new_project_publishes = [] self.__project_name_template = self._app.get_template( "template_new_project_name") def create_new_project(self, name_part, sg_publish_data): """ Create a new project in the current Toolkit context and seed it with the specified geometry :param name: The name to use in the project_name template when generating the project name :param sg_publish_data: List of the initial geometry publishes to load for into the new project. Each entry in the list is a Shotgun entity dictionary :returns: The new Mari project instance if successful or None if not :raises: TankError if something went wrong at any stage! """ # create the project name: name_result = self._generate_new_project_name(name_part) project_name = name_result.get("project_name") if not project_name: raise TankError("Failed to determine the project name: %s" % name_result.get("message")) # use a hook to retrieve the project creation settings to use: hook_res = {} try: hook_res = self._app.execute_hook_method( "get_project_creation_args_hook", "get_project_creation_args", sg_publish_data=sg_publish_data) if hook_res == None: hook_res = {} elif not isinstance(hook_res, dict): raise TankError( "get_project_creation_args_hook returned unexpected type!") except TankError, e: raise TankError( "Failed to get project creation args from hook: %s" % e) except Exception, e: self._app.log_exception( "Failed to get project creation args from hook!") raise TankError( "Failed to get project creation args from hook: %s" % e)
def flame_minor_version(self): """ Returns Flame's minor version number as a string. :returns: String (e.g. '2') """ if self._flame_version is None: raise TankError("No Flame DCC version specified!") return self._flame_version["minor"]
def _get_publish_name(self, item, task_settings): """ Get the publish name for the supplied item. :param item: The item to determine the publish version for Uses the path info hook to retrieve the publish name. """ publisher = self.parent # Start with the item's fields fields = copy.copy(item.properties.get("fields", {})) publish_name_template = task_settings.get("publish_name_template") publish_name = None # First check if we have a publish_name_template defined and attempt to # get the publish name from that if publish_name_template: pub_tmpl = publisher.get_template_by_name(publish_name_template) if not pub_tmpl: # this template was not found in the template config! raise TankError("The Template '%s' does not exist!" % publish_name_template) # First get the fields from the context try: fields.update(item.context.as_template_fields(pub_tmpl)) except TankError, e: self.logger.debug( "Unable to get context fields for publish_name_template.") missing_keys = pub_tmpl.missing_keys(fields, True) if missing_keys: raise TankError( "Cannot resolve publish_name_template (%s). Missing keys: %s" % (publish_name_template, pprint.pformat(missing_keys))) publish_name = pub_tmpl.apply_fields(fields) self.logger.debug( "Retrieved publish_name via publish_name_template.")
def _prepare_flame_flare_launch(engine_name, context, app_path, app_args): """ Flame specific pre-launch environment setup. :param engine_name: The name of the engine being launched (tk-flame or tk-flare) :param context: The context that the application is being launched in :param app_path: Path to DCC executable or launch script :param app_args: External app arguments :returns: Tuple (app_path, app_args) Potentially modified app_path or app_args value, depending on preparation requirements for flame. """ # Retrieve the TK Application instance from the current bundle tk_app = sgtk.platform.current_bundle() # find the path to the engine on disk where the startup script can be found: engine_path = sgtk.platform.get_engine_path(engine_name, tk_app.sgtk, context) if engine_path is None: raise TankError("Path to '%s' engine could not be found." % engine_name) # find bootstrap file located in the engine and load that up startup_path = os.path.join(engine_path, "python", "startup", "bootstrap.py") if not os.path.exists(startup_path): raise Exception("Cannot find bootstrap script '%s'" % startup_path) python_path = os.path.dirname(startup_path) # add our bootstrap location to the pythonpath sys.path.insert(0, python_path) try: import bootstrap (app_path, new_args) = bootstrap.bootstrap(engine_name, context, app_path, app_args) except Exception, e: tk_app.log_exception("Error executing engine bootstrap script.") if tk_app.engine.has_ui: # got UI support. Launch dialog with nice message not_found_dialog = tk_app.import_module("not_found_dialog") not_found_dialog.show_generic_error_dialog(tk_app, str(e)) raise TankError("Error executing bootstrap script. Please see log for details.")
def add_to_change(p4, change, file_paths): """ Add the specified files to the specified change """ try: # use reopen command which works with local file paths. # fetch/modify/save_change only works with depot paths! p4.run_reopen("-c", str(change), file_paths) except P4Exception, e: raise TankError("Perforce: %s" % (p4.errors[0] if p4.errors else e))
def flame_version(self): """ Returns Flame's full version number as a string. :returns: String (e.g. '2016.1.0.278') """ if self._flame_version is None: raise TankError("No Flame DCC version specified!") return self._flame_version["full"]
def _get_actions_for_publish(self, sg_data, ui_area): """ Retrieves the list of actions for a given publish. :param sg_data: Publish to retrieve actions for :param ui_area: Indicates which part of the UI the request is coming from. Currently one of UI_AREA_MAIN, UI_AREA_DETAILS and UI_AREA_HISTORY :return: List of actions. """ # Figure out the type of the publish publish_type_dict = sg_data.get(self._publish_type_field) if publish_type_dict is None: # this publish does not have a type publish_type = "undefined" else: publish_type = publish_type_dict["name"] # check if we have logic configured to handle this publish type. mappings = self._app.get_setting("action_mappings") # returns a structure on the form # { "Maya Scene": ["reference", "import"] } actions = mappings.get(publish_type, []) if len(actions) == 0: return [] # cool so we have one or more actions for this publish type. # resolve UI area if ui_area == LoaderActionManager.UI_AREA_DETAILS: ui_area_str = "details" elif ui_area == LoaderActionManager.UI_AREA_HISTORY: ui_area_str = "history" elif ui_area == LoaderActionManager.UI_AREA_MAIN: ui_area_str = "main" else: raise TankError("Unsupported UI_AREA. Contact support.") # convert created_at unix time stamp to shotgun time stamp self._fix_timestamp(sg_data) action_defs = [] try: # call out to hook to give us the specifics. action_defs = self._app.execute_hook_method( "actions_hook", "generate_actions", sg_publish_data=sg_data, actions=actions, ui_area=ui_area_str, ) except Exception: self._app.log_exception("Could not execute generate_actions hook.") return action_defs
def _get_current_project(self): """ Returns the current project based on where in the UI the user clicked """ # get the menu selection from hiero engine selection = self.parent.engine.get_menu_selection() if len(selection) != 1: raise TankError("Please select a single Project!") if not isinstance(selection[0], hiero.core.Bin): raise TankError("Please select a Hiero Project!") project = selection[0].project() if project is None: # apparently bins can be without projects (child bins I think) raise TankError("Please select a Hiero Project!") return project
def execute(self, **kwargs): """ Main hook entry point :returns: A list of any items that were found to be published. Each item in the list should be a dictionary containing the following keys: { type: String This should match a scene_item_type defined in one of the outputs in the configuration and is used to determine the outputs that should be published for the item name: String Name to use for the item in the UI description: String Description of the item to use in the UI selected: Bool Initial selected state of item in the UI. Items are selected by default. required: Bool Required state of item in the UI. If True then item will not be deselectable. Items are not required by default. other_params: Dictionary Optional dictionary that will be passed to the pre-publish and publish hooks } """ items = [] # get the main scene: if not MaxPlus.FileManager.GetFileName(): raise TankError("Please Save your file before Publishing") # create the primary item - 'type' should match the 'primary_scene_item_type': items.append({ "type": "work_file", "name": MaxPlus.FileManager.GetFileName() }) # always add a secondary item to allow user to commit all changes to Perforce: # Note: only need one of these as it submits all published files items.append({ "type": "perforce_submit", "name": "All Published Files" }) return items
def _get_active_document(self): """ Returns the currently open document in Photoshop. Raises an exeption if no document is active. """ doc = self.parent.engine.adobe.get_active_document() if not doc: raise TankError("There is no active document!") return doc
def __init__(self, bundle): """ Constructor :param bundle: app, engine or framework object to associate the settings with. """ self.__fw = sgtk.platform.current_bundle() self.__settings = QtCore.QSettings("Shotgun Software", bundle.name) self.__fw.log_debug("Initialized settings manager for '%s'" % bundle.name) # now organize various keys # studio level settings - base it on the server host name _, sg_hostname, _, _, _ = urlparse.urlsplit(self.__fw.sgtk.shotgun_url) self.__site_key = sg_hostname # project level settings pc = self.__fw.sgtk.pipeline_configuration self.__project_key = "%s:%s" % (self.__site_key, pc.get_project_disk_name()) # config level settings self.__pipeline_config_key = "%s:%s" % (self.__project_key, pc.get_name()) # instance level settings if isinstance(bundle, sgtk.platform.Application): # based on the environment name, engine instance name and app instance name self.__instance_key = "%s:%s:%s:%s" % ( self.__pipeline_config_key, bundle.engine.environment["name"], bundle.engine.instance_name, bundle.instance_name) self.__engine_key = bundle.engine.name elif isinstance(bundle, sgtk.platform.Engine): # based on the environment name & engine instance name self.__instance_key = "%s:%s:%s" % (self.__pipeline_config_key, bundle.environment["name"], bundle.instance_name) self.__engine_key = bundle.name elif isinstance(bundle, sgtk.platform.Framework): # based on the environment name & framework name self.__instance_key = "%s:%s:%s" % ( self.__pipeline_config_key, bundle.engine.environment["name"], bundle.name) self.__engine_key = bundle.engine.name else: raise TankError("Not sure how to handle bundle type %s. " "Please pass an App, Engine or Framework object." % bundle)