From d88ed919e6a4a566b8ff8b289415c471de454b00 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 16 Mar 2022 22:09:23 +0100 Subject: [PATCH 001/124] First draft pass of refactoring the Integrator --- openpype/plugins/publish/integrate_new.py | 1076 ++++++++++----------- 1 file changed, 508 insertions(+), 568 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e8dab089af..e4986e3b3f 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -7,9 +7,8 @@ import clique import errno import six import re -import shutil -from pymongo import DeleteOne, InsertOne +from pymongo import DeleteOne, InsertOne, UpdateOne import pyblish.api from avalon import io from avalon.api import format_template_with_optional_keys @@ -31,6 +30,17 @@ else: log = logging.getLogger(__name__) +def get_frame_padded(frame, padding): + """Return frame number as string with `padding` amount of padded zeros""" + return "{frame:0{padding}d}".format(padding=padding, frame=frame) + + +def get_first_frame_padded(collection): + """Return first frame as padded number from `clique.Collection`""" + start_frame = next(iter(collection.indexes)) + return get_frame_padded(start_frame, padding=collection.padding) + + class IntegrateAssetNew(pyblish.api.InstancePlugin): """Resolve any dependency issues @@ -108,7 +118,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): exclude_families = ["clip"] db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", - "family", "hierarchy", "task", "username" + "family", "hierarchy", "task", "username", "frame", "udim" ] default_template_name = "publish" @@ -116,38 +126,40 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): TMP_FILE_EXT = 'tmp' # file_url : file_size of all published and uploaded files - integrated_file_sizes = {} + destinations = list() # Attributes set by settings template_name_profiles = None subset_grouping_profiles = None def process(self, instance): - self.integrated_file_sizes = {} - if [ef for ef in self.exclude_families - if instance.data["family"] in ef]: + self.destinations = [] + + # Exclude instances that also contain families from exclude families + families = set( + # Consider family and families data + [instance.data["family"]] + instance.data.get("families", []) + ) + if families & set(self.exclude_families): return try: self.register(instance) self.log.info("Integrated Asset in to the database ...") - self.log.info("instance.data: {}".format(instance.data)) - self.handle_destination_files(self.integrated_file_sizes, + self.handle_destination_files(self.destinations, 'finalize') except Exception: # clean destination self.log.critical("Error when registering", exc_info=True) - self.handle_destination_files(self.integrated_file_sizes, 'remove') + self.handle_destination_files(self.destinations, 'remove') six.reraise(*sys.exc_info()) - def register(self, instance): - # Required environment variables - anatomy_data = instance.data["anatomyData"] - - io.install() + def prepare_anatomy(self, instance): + """Prepare anatomy data used to define representation destinations""" context = instance.context + anatomy_data = instance.data["anatomyData"] project_entity = instance.data["projectEntity"] context_asset_name = None @@ -206,8 +218,36 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Fill family in anatomy data anatomy_data["family"] = instance.data.get("family") - stagingdir = instance.data.get("stagingDir") - if not stagingdir: + intent_value = instance.context.data.get("intent") + if intent_value and isinstance(intent_value, dict): + intent_value = intent_value.get("value") + + if intent_value: + anatomy_data["intent"] = intent_value + + # Get profile + key_values = { + "families": self.main_family_from_instance(instance), + "tasks": task_name, + "hosts": instance.context.data["hostName"], + "task_types": task_type + } + profile = filter_profiles( + self.template_name_profiles, + key_values, + logger=self.log + ) + + template_name = "publish" + if profile: + template_name = profile["template_name"] + + return template_name, anatomy_data + + def register(self, instance): + + instance_stagingdir = instance.data.get("stagingDir") + if not instance_stagingdir: self.log.info(( "{0} is missing reference to staging directory." " Will try to get it from representation." @@ -215,7 +255,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): else: self.log.debug( - "Establishing staging directory @ {0}".format(stagingdir) + "Establishing staging directory " + "@ {0}".format(instance_stagingdir) ) # Ensure at least one file is set up for transfer in staging dir. @@ -227,28 +268,74 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) ) - subset = self.get_subset(asset_entity, instance) - instance.data["subsetEntity"] = subset + subset = self.register_subset(instance) + + version = self.register_version(instance, subset) + instance.data["versionEntity"] = version + instance.data['version'] = version['name'] + + existing_repres = list(io.find({ + "parent": version["_id"], + "type": "archived_representation" + })) + + # Find the representations to transfer amongst the files + # Each should be a single representation (as such, a single extension) + template_name, anatomy_data = self.prepare_anatomy(instance) + published_representations = {} + representations = [] + for repre in instance.data["representations"]: + + if "delete" in repre.get("tags", []): + self.log.debug("Skipping representation marked for deletion: " + "{}".format(repre)) + continue + + prepared = self.prepare_representation(repre, + anatomy_data, + template_name, + existing_repres, + version, + instance_stagingdir, + instance) + + # todo: simplify this? + representation = prepared["representation"] + representations.append(representation) + published_representations[representation["_id"]] = prepared + + # Remove old representations if there are any (before insertion of new) + if existing_repres: + repre_ids_to_remove = [repre["_id"] for repre in existing_repres] + io.delete_many({"_id": {"$in": repre_ids_to_remove}}) + + # Write the new representations to the database + io.insert_many(representations) + + instance.data["published_representations"] = published_representations + + self.log.info("Registered {} representations" + "".format(len(representations))) + + def register_version(self, instance, subset): version_number = instance.data["version"] self.log.debug("Next version: v{}".format(version_number)) - version_data = self.create_version_data(context, instance) - + version_data = self.create_version_data(instance) version_data_instance = instance.data.get('versionData') if version_data_instance: version_data.update(version_data_instance) - # TODO rename method from `create_version` to - # `prepare_version` or similar... - version = self.create_version( - subset=subset, - version_number=version_number, - data=version_data - ) - - self.log.debug("Creating version ...") + version = { + "schema": "openpype:version-3.0", + "type": "version", + "parent": subset["_id"], + "name": version_number, + "data": version_data + } + repres = instance.data.get("representations", []) new_repre_names_low = [_repre["name"].lower() for _repre in repres] existing_version = io.find_one({ @@ -258,29 +345,28 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): }) if existing_version is None: + self.log.debug("Creating new version ...") version_id = io.insert_one(version).inserted_id else: + self.log.debug("Updating existing version ...") # Check if instance have set `append` mode which cause that # only replicated representations are set to archive append_repres = instance.data.get("append", False) + bulk_writes = [] # Update version data - # TODO query by _id and - io.update_many({ - 'type': 'version', - 'parent': subset["_id"], - 'name': version_number + version_id = existing_version['_id'] + bulk_writes.append(UpdateOne({ + '_id': version_id }, { '$set': version - }) - version_id = existing_version['_id'] + })) # Find representations of existing version and archive them - current_repres = list(io.find({ + current_repres = io.find({ "type": "representation", "parent": version_id - })) - bulk_writes = [] + }) for repre in current_repres: if append_repres: # archive only duplicated representations @@ -304,346 +390,248 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) version = io.find_one({"_id": version_id}) - instance.data["versionEntity"] = version + return version - existing_repres = list(io.find({ - "parent": version_id, - "type": "archived_representation" - })) + def prepare_representation(self, repre, + anatomy_data, + template_name, + existing_repres, + version, + instance_stagingdir, + instance): - instance.data['version'] = version['name'] + # create template data for Anatomy + template_data = copy.deepcopy(anatomy_data) - intent_value = instance.context.data.get("intent") - if intent_value and isinstance(intent_value, dict): - intent_value = intent_value.get("value") + # pre-flight validations + if repre["ext"].startswith("."): + raise ValueError("Extension must not start with a dot '.': " + "{}".format(repre["ext"])) - if intent_value: - anatomy_data["intent"] = intent_value + if repre.get("transfers"): + raise ValueError("Representation is not allowed to have transfers" + "data before integration. " + "Got: {}".format(repre["transfers"])) - anatomy = instance.context.data['anatomy'] + # required representation keys + files = repre['files'] + template_data["representation"] = repre["name"] + template_data["ext"] = repre["ext"] - # Find the representations to transfer amongst the files - # Each should be a single representation (as such, a single extension) - representations = [] - destination_list = [] + # optionals + # retrieve additional anatomy data from representation if exists + for representation_key, anatomy_key in { + # Representation Key: Anatomy data key + "resolutionWidth": "resolution_width", + "resolutionHeight": "resolution_height", + "fps": "fps", + "outputName": "output", + }.items(): + value = repre.get(representation_key) + if value: + template_data[anatomy_key] = value - orig_transfers = [] - if 'transfers' not in instance.data: - instance.data['transfers'] = [] + if repre.get('stagingDir'): + stagingdir = repre['stagingDir'] else: - orig_transfers = list(instance.data['transfers']) + # Fall back to instance staging dir if not explicitly + # set for representation in the instance + self.log.debug("Representation uses instance staging dir: " + "{}".format(instance_stagingdir)) + stagingdir = instance_stagingdir - family = self.main_family_from_instance(instance) + self.log.debug("Anatomy template name: {}".format(template_name)) + anatomy = instance.context.data['anatomy'] + template = os.path.normpath( + anatomy.templates[template_name]["path"]) - key_values = { - "families": family, - "tasks": task_name, - "hosts": instance.context.data["hostName"], - "task_types": task_type - } - profile = filter_profiles( - self.template_name_profiles, - key_values, - logger=self.log - ) + is_sequence_representation = isinstance(files, (list, tuple)) + if is_sequence_representation: + # Collection of files (sequence) + # Get the sequence as a collection. The files must be of a single + # sequence and have no remainder outside of the collections. + collections, remainder = clique.assemble(files, + minimum_items=1) + if not collections: + raise ValueError("No collections found in files: " + "{}".format(files)) + if remainder: + raise ValueError("Files found not detected as part" + " of a sequence: {}".format(remainder)) + if len(collections) > 1: + raise ValueError("Files in sequence are not part of a" + " single sequence collection: " + "{}".format(collections)) + src_collection = collections[0] - template_name = "publish" - if profile: - template_name = profile["template_name"] - - published_representations = {} - for idx, repre in enumerate(instance.data["representations"]): - # reset transfers for next representation - # instance.data['transfers'] is used as a global variable - # in current codebase - instance.data['transfers'] = list(orig_transfers) - - if "delete" in repre.get("tags", []): - continue - - published_files = [] - - # create template data for Anatomy - template_data = copy.deepcopy(anatomy_data) - if intent_value is not None: - template_data["intent"] = intent_value - - resolution_width = repre.get("resolutionWidth") - resolution_height = repre.get("resolutionHeight") - fps = instance.data.get("fps") - - if resolution_width: - template_data["resolution_width"] = resolution_width - if resolution_width: - template_data["resolution_height"] = resolution_height - if resolution_width: - template_data["fps"] = fps - - files = repre['files'] - if repre.get('stagingDir'): - stagingdir = repre['stagingDir'] - - if repre.get("outputName"): - template_data["output"] = repre['outputName'] - - template_data["representation"] = repre["name"] - - ext = repre["ext"] - if ext.startswith("."): - self.log.warning(( - "Implementaion warning: <\"{}\">" - " Representation's extension stored under \"ext\" key " - " started with dot (\"{}\")." - ).format(repre["name"], ext)) - ext = ext[1:] - repre["ext"] = ext - template_data["ext"] = ext - - self.log.info(template_name) - template = os.path.normpath( - anatomy.templates[template_name]["path"]) - - sequence_repre = isinstance(files, list) - repre_context = None - if sequence_repre: - self.log.debug( - "files: {}".format(files)) - src_collections, remainder = clique.assemble(files) - self.log.debug( - "src_tail_collections: {}".format(str(src_collections))) - src_collection = src_collections[0] - - # Assert that each member has identical suffix - src_head = src_collection.format("{head}") - src_tail = src_collection.format("{tail}") - - # fix dst_padding - valid_files = [x for x in files if src_collection.match(x)] - padd_len = len( - valid_files[0].replace(src_head, "").replace(src_tail, "") - ) - src_padding_exp = "%0{}d".format(padd_len) - - test_dest_files = list() - for i in [1, 2]: - template_data["representation"] = repre['ext'] - if not repre.get("udim"): - template_data["frame"] = src_padding_exp % i - else: - template_data["udim"] = src_padding_exp % i - - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] - if repre_context is None: - repre_context = template_filled.used_values - test_dest_files.append( - os.path.normpath(template_filled) - ) - if not repre.get("udim"): - template_data["frame"] = repre_context["frame"] - else: - template_data["udim"] = repre_context["udim"] - - self.log.debug( - "test_dest_files: {}".format(str(test_dest_files))) - - dst_collections, remainder = clique.assemble(test_dest_files) - dst_collection = dst_collections[0] - dst_head = dst_collection.format("{head}") - dst_tail = dst_collection.format("{tail}") - - index_frame_start = None + # If the representation has `frameStart` set it renumbers the + # frame indices of the published collection. It will start from + # that `frameStart` index instead. Thus if that frame start + # differs from the collection we want to shift the destination + # frame indices from the source collection. + destination_indexes = list(src_collection.indexes) + destination_padding = len(get_first_frame_padded(src_collection)) + if repre.get("frameStart") is not None: + index_frame_start = int(repre.get("frameStart")) # TODO use frame padding from right template group - if repre.get("frameStart") is not None: - frame_start_padding = int( - anatomy.templates["render"].get( - "frame_padding", - anatomy.templates["render"].get("padding") - ) + render_template = anatomy.templates["render"] + frame_start_padding = int( + render_template.get( + "frame_padding", + render_template.get("padding") ) - - index_frame_start = int(repre.get("frameStart")) - - # exception for slate workflow - if index_frame_start and "slate" in instance.data["families"]: - index_frame_start -= 1 - - dst_padding_exp = src_padding_exp - dst_start_frame = None - collection_start = list(src_collection.indexes)[0] - for i in src_collection.indexes: - # TODO 1.) do not count padding in each index iteration - # 2.) do not count dst_padding from src_padding before - # index_frame_start check - frame_number = i - collection_start - src_padding = src_padding_exp % i - - src_file_name = "{0}{1}{2}".format( - src_head, src_padding, src_tail) - - dst_padding = src_padding_exp % frame_number - - if index_frame_start is not None: - dst_padding_exp = "%0{}d".format(frame_start_padding) - dst_padding = dst_padding_exp % (index_frame_start + frame_number) # noqa: E501 - elif repre.get("udim"): - dst_padding = int(i) - - dst = "{0}{1}{2}".format( - dst_head, - dst_padding, - dst_tail - ) - - self.log.debug("destination: `{}`".format(dst)) - src = os.path.join(stagingdir, src_file_name) - - self.log.debug("source: {}".format(src)) - instance.data["transfers"].append([src, dst]) - - published_files.append(dst) - - # for adding first frame into db - if not dst_start_frame: - dst_start_frame = dst_padding - - # Store used frame value to template data - if repre.get("frame"): - template_data["frame"] = dst_start_frame - - dst = "{0}{1}{2}".format( - dst_head, - dst_start_frame, - dst_tail - ) - repre['published_path'] = dst - - else: - # Single file - # _______ - # | |\ - # | | - # | | - # | | - # |_______| - # - template_data.pop("frame", None) - fname = files - assert not os.path.isabs(fname), ( - "Given file name is a full path" ) - template_data["representation"] = repre['ext'] - # Store used frame value to template data - if repre.get("udim"): - template_data["udim"] = repre["udim"][0] - src = os.path.join(stagingdir, fname) - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] - repre_context = template_filled.used_values - dst = os.path.normpath(template_filled) - - instance.data["transfers"].append([src, dst]) - - published_files.append(dst) - repre['published_path'] = dst - self.log.debug("__ dst: {}".format(dst)) + # Shift destination sequence to the start frame + src_start_frame = next(iter(src_collection.indexes)) + shift = index_frame_start - src_start_frame + if shift: + destination_indexes = [ + frame + shift for frame in destination_indexes + ] + destination_padding = frame_start_padding + # To construct the destination template with anatomy we require + # a Frame or UDIM tile set for the template data. We use the first + # index of the destination for that because that could've shifted + # from the source indexes, etc. + first_index_padded = get_frame_padded(frame=destination_indexes[0], + padding=destination_padding) if repre.get("udim"): - repre_context["udim"] = repre.get("udim") # store list + # UDIM representations handle ranges in a different manner + template_data["udim"] = first_index_padded + else: + template_data["frame"] = first_index_padded - repre["publishedFiles"] = published_files + # Construct destination collection from template + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled[template_name]["path"] + repre_context = template_filled.used_values + self.log.debug("Template filled: {}".format(str(template_filled))) + dst_collections, _remainder = clique.assemble( + [os.path.normpath(template_filled)], minimum_items=1 + ) + assert not _remainder, "This is a bug" + assert len(dst_collections) == 1, "This is a bug" + dst_collection = dst_collections[0] - for key in self.db_representation_context_keys: - value = template_data.get(key) - if not value: - continue - repre_context[key] = template_data[key] + # Update the destination indexes and padding + dst_collection.indexes = destination_indexes + dst_collection.padding = destination_padding + assert len(src_collection) == len(dst_collection), "This is a bug" - # Use previous representation's id if there are any - repre_id = None - repre_name_low = repre["name"].lower() - for _repre in existing_repres: - # NOTE should we check lowered names? - if repre_name_low == _repre["name"]: - repre_id = _repre["orig_id"] - break + transfers = [] + for src_file_name, dst in zip(src_collection, dst_collection): + src = os.path.join(stagingdir, src_file_name) + self.log.debug("source: {}".format(src)) + self.log.debug("destination: `{}`".format(dst)) + transfers.append(src, dst) - # Create new id if existing representations does not match - if repre_id is None: - repre_id = io.ObjectId() + # Store first frame as published path + # todo: remove `published_path` since it can be retrieved from + # `transfers` by taking the first destination transfers[0][1] + repre['published_path'] = next(iter(dst_collection)) + repre["transfers"].extend(transfers) - data = repre.get("data") or {} - data.update({'path': dst, 'template': template}) - representation = { - "_id": repre_id, - "schema": "openpype:representation-2.0", - "type": "representation", - "parent": version_id, - "name": repre['name'], - "data": data, - "dependencies": instance.data.get("dependencies", "").split(), + else: + # Single file + template_data.pop("frame", None) + fname = files + assert not os.path.isabs(fname), ( + "Given file name is a full path" + ) + # Store used frame value to template data + if repre.get("udim"): + template_data["udim"] = repre["udim"][0] + src = os.path.join(stagingdir, fname) + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled[template_name]["path"] + repre_context = template_filled.used_values + dst = os.path.normpath(template_filled) - # Imprint shortcut to context - # for performance reasons. - "context": repre_context - } + # Single file transfer + self.log.debug("source: {}".format(src)) + self.log.debug("destination: `{}`".format(dst)) + repre["transfers"] = [src, dst] - if repre.get("outputName"): - representation["context"]["output"] = repre['outputName'] + repre['published_path'] = dst - if sequence_repre and repre.get("frameStart") is not None: - representation['context']['frame'] = ( - dst_padding_exp % int(repre.get("frameStart")) - ) + if repre.get("udim"): + repre_context["udim"] = repre.get("udim") # store list - # any file that should be physically copied is expected in - # 'transfers' or 'hardlinks' - if instance.data.get('transfers', False) or \ - instance.data.get('hardlinks', False): - # could throw exception, will be caught in 'process' - # all integration to DB is being done together lower, - # so no rollback needed - self.log.debug("Integrating source files to destination ...") - self.integrated_file_sizes.update(self.integrate(instance)) - self.log.debug("Integrated files {}". - format(self.integrated_file_sizes)) + for key in self.db_representation_context_keys: + value = template_data.get(key) + if not value: + continue + repre_context[key] = template_data[key] - # get 'files' info for representation and all attached resources - self.log.debug("Preparing files information ...") - representation["files"] = self.get_files_info( - instance, - self.integrated_file_sizes) + # Use previous representation's id if there are any + repre_id = None + repre_name_lower = repre["name"].lower() + for _existing_repre in existing_repres: + # NOTE should we check lowered names? + if repre_name_lower == _existing_repre["name"].lower(): + repre_id = _existing_repre["orig_id"] + break - self.log.debug("__ representation: {}".format(representation)) - destination_list.append(dst) - self.log.debug("__ destination_list: {}".format(destination_list)) - instance.data['destination_list'] = destination_list - representations.append(representation) - published_representations[repre_id] = { - "representation": representation, - "anatomy_data": template_data, - "published_files": published_files - } - self.log.debug("__ representations: {}".format(representations)) + # Create new id if existing representations does not match + if repre_id is None: + repre_id = io.ObjectId() - # Remove old representations if there are any (before insertion of new) - if existing_repres: - repre_ids_to_remove = [] - for repre in existing_repres: - repre_ids_to_remove.append(repre["_id"]) - io.delete_many({"_id": {"$in": repre_ids_to_remove}}) + # todo: `repre` is not the actual `representation` entity + # we should simplify/clarify difference between data above + # and the actual representation entity for the database + data = repre.get("data") or {} + data.update({'path': dst, 'template': template}) + representation = { + "_id": repre_id, + "schema": "openpype:representation-2.0", + "type": "representation", + "parent": version["_id"], + "name": repre['name'], + "data": data, + "dependencies": instance.data.get("dependencies", "").split(), - for rep in instance.data["representations"]: - self.log.debug("__ rep: {}".format(rep)) + # Imprint shortcut to context for performance reasons. + "context": repre_context + } - io.insert_many(representations) - instance.data["published_representations"] = ( - published_representations + if repre.get("outputName"): + representation["context"]["output"] = repre['outputName'] + + if is_sequence_representation and repre.get("frameStart") is not None: + representation['context']['frame'] = template_data["frame"] + + # any file that should be physically copied is expected in + # 'transfers' or 'hardlinks' + integrated_files = [] + if instance.data.get('transfers', False) or \ + instance.data.get('hardlinks', False): + # could throw exception, will be caught in 'process' + # all integration to DB is being done together lower, + # so no rollback needed + # todo: separate the actual integrating of the files onto its own + # taking just a list of transfers as inputs (potentially + # with copy mode flag, like hardlink/copy, etc.) + self.log.debug("Integrating source files to destination ...") + integrated_files = self.integrate(instance) + self.log.debug("Integrated files {}".format(integrated_files)) + + # get 'files' info for representation and all attached resources + self.log.debug("Preparing files information ...") + representation["files"] = self.get_files_info( + instance, + integrated_files ) - # self.log.debug("Representation: {}".format(representations)) - self.log.info("Registered {} items".format(len(representations))) + + return { + "representation": representation, + "anatomy_data": template_data, + # todo: avoid the need for 'published_files'? + # backwards compatibility + "published_files": [transfer[1] for transfer in repre["transfers"]] + } def integrate(self, instance): """ Move the files. @@ -653,92 +641,93 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): Args: instance: the instance to integrate Returns: - integrated_file_sizes: dictionary of destination file url and - its size in bytes + list: destination full paths of integrated files """ - # store destination url and size for reporting and rollback - integrated_file_sizes = {} + # store destinations for potential rollback and measuring sizes + destinations = [] transfers = list(instance.data.get("transfers", list())) for src, dest in transfers: - if os.path.normpath(src) != os.path.normpath(dest): + src = os.path.normpath(src) + dest = os.path.normpath(dest) + if src != dest: dest = self.get_dest_temp_url(dest) self.copy_file(src, dest) - # TODO needs to be updated during site implementation - integrated_file_sizes[dest] = os.path.getsize(dest) + destinations.append(dest) # Produce hardlinked copies - # Note: hardlink can only be produced between two files on the same - # server/disk and editing one of the two will edit both files at once. - # As such it is recommended to only make hardlinks between static files - # to ensure publishes remain safe and non-edited. hardlinks = instance.data.get("hardlinks", list()) for src, dest in hardlinks: dest = self.get_dest_temp_url(dest) - self.log.debug("Hardlinking file ... {} -> {}".format(src, dest)) if not os.path.exists(dest): self.hardlink_file(src, dest) - # TODO needs to be updated during site implementation - integrated_file_sizes[dest] = os.path.getsize(dest) + destinations.append(dest) - return integrated_file_sizes + return destinations + + def _create_folder_for_file(self, path): + dirname = os.path.dirname(path) + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + six.reraise(*sys.exc_info()) def copy_file(self, src, dst): - """ Copy given source to destination + """Copy source filepath to destination filepath Arguments: src (str): the source file which needs to be copied - dst (str): the destination of the sourc file + dst (str): the destination filepath + + Returns: + None + + """ + self._create_folder_for_file(dst) + self.log.debug("Copying file ... {} -> {}".format(src, dst)) + copyfile(src, dst) + + def hardlink_file(self, src, dst): + """Hardlink source filepath to destination filepath. + + Note: + Hardlink can only be produced between two files on the same + server/disk and editing one of the two will edit both files at + once. As such it is recommended to only make hardlinks between + static files to ensure publishes remain safe and non-edited. + + Arguments: + src (str): the source file which needs to be hardlinked + dst (str): the destination filepath + Returns: None """ - src = os.path.normpath(src) - dst = os.path.normpath(dst) - self.log.debug("Copying file ... {} -> {}".format(src, dst)) - dirname = os.path.dirname(dst) - try: - os.makedirs(dirname) - except OSError as e: - if e.errno == errno.EEXIST: - pass - else: - self.log.critical("An unexpected error occurred.") - six.reraise(*sys.exc_info()) - - # copy file with speedcopy and check if size of files are simetrical - while True: - if not shutil._samefile(src, dst): - copyfile(src, dst) - else: - self.log.critical( - "files are the same {} to {}".format(src, dst) - ) - os.remove(dst) - try: - shutil.copyfile(src, dst) - self.log.debug("Copying files with shutil...") - except OSError as e: - self.log.critical("Cannot copy {} to {}".format(src, dst)) - self.log.critical(e) - six.reraise(*sys.exc_info()) - if str(getsize(src)) in str(getsize(dst)): - break - - def hardlink_file(self, src, dst): - dirname = os.path.dirname(dst) - - try: - os.makedirs(dirname) - except OSError as e: - if e.errno == errno.EEXIST: - pass - else: - self.log.critical("An unexpected error occurred.") - six.reraise(*sys.exc_info()) - + self._create_folder_for_file(dst) + self.log.debug("Hardlinking file ... {} -> {}".format(src, dst)) create_hard_link(src, dst) - def get_subset(self, asset, instance): + def _get_instance_families(self, instance): + """Get all families of the instance""" + # todo: move this to lib? + family = instance.data.get("family") + families = [] + if family: + families.append(family) + + for _family in (instance.data.get("families") or []): + if _family not in families: + families.append(_family) + + return families + + def register_subset(self, instance): + # todo: rely less on self.prepare_anatomy to create this value + asset = instance.data.get("assetEntity") # <- from prepare_anatomy :( subset_name = instance.data["subset"] subset = io.find_one({ "type": "subset", @@ -748,18 +737,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if subset is None: self.log.info("Subset '%s' not found, creating ..." % subset_name) - self.log.debug("families. %s" % instance.data.get('families')) - self.log.debug( - "families. %s" % type(instance.data.get('families'))) - - family = instance.data.get("family") - families = [] - if family: - families.append(family) - - for _family in (instance.data.get("families") or []): - if _family not in families: - families.append(_family) + families = self._get_instance_families(instance) _id = io.insert_one({ "schema": "openpype:subset-3.0", @@ -773,8 +751,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): subset = io.find_one({"_id": _id}) - # QUESTION Why is changing of group and updating it's - # families in 'get_subset'? + # Update subset group self._set_subset_group(instance, subset["_id"]) # Update families on subset. @@ -838,7 +815,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.subset_grouping_profiles, filtering_criteria ) - # Skip if there is not matchin profile + # Skip if there is not matching profile if not matching_profile: return None @@ -867,41 +844,17 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return filled_template - def create_version(self, subset, version_number, data=None): - """ Copy given source to destination - - Args: - subset (dict): the registered subset of the asset - version_number (int): the version number - - Returns: - dict: collection of data to create a version - """ - - return {"schema": "openpype:version-3.0", - "type": "version", - "parent": subset["_id"], - "name": version_number, - "data": data} - - def create_version_data(self, context, instance): + def create_version_data(self, instance): """Create the data collection for the version Args: - context: the current context instance: the current instance being published Returns: dict: the required information with instance.data as key """ - families = [] - current_families = instance.data.get("families", list()) - instance_family = instance.data.get("family", None) - - if instance_family is not None: - families.append(instance_family) - families += current_families + context = instance.context # create relative source path for DB if "source" in instance.data: @@ -910,10 +863,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): source = context.data["currentFile"] anatomy = instance.context.data["anatomy"] source = self.get_rootless_path(anatomy, source) - self.log.debug("Source: {}".format(source)) + version_data = { - "families": families, + "families": self._get_instance_families(instance), "time": context.data["time"], "author": context.data["user"], "source": source, @@ -924,7 +877,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) } - intent_value = instance.context.data.get("intent") + intent_value = context.data.get("intent") if intent_value and isinstance(intent_value, dict): intent_value = intent_value.get("value") @@ -944,10 +897,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def main_family_from_instance(self, instance): """Returns main family of entered instance.""" - family = instance.data.get("family") - if not family: - family = instance.data["families"][0] - return family + return self._get_instance_families(instance)[0] def get_rootless_path(self, anatomy, path): """ Returns, if possible, path without absolute portion from host @@ -976,7 +926,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ).format(path)) return path - def get_files_info(self, instance, integrated_file_sizes): + def get_files_info(self, instance): """ Prepare 'files' portion for attached resources and main asset. Combining records from 'transfers' and 'hardlinks' parts from instance. @@ -991,27 +941,18 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): output_resources: array of dictionaries to be added to 'files' key in representation """ + # todo: refactor to use transfers/hardlinks of representations + # currently broken logic resources = list(instance.data.get("transfers", [])) resources.extend(list(instance.data.get("hardlinks", []))) + self.log.debug("get_files_info.resources:{}".format(resources)) - self.log.debug("get_resource_files_info.resources:{}". - format(resources)) + sites = self.compute_resource_sync_sites(instance) output_resources = [] anatomy = instance.context.data["anatomy"] for _src, dest in resources: - path = self.get_rootless_path(anatomy, dest) - dest = self.get_dest_temp_url(dest) - file_hash = openpype.api.source_hash(dest) - if self.TMP_FILE_EXT and \ - ',{}'.format(self.TMP_FILE_EXT) in file_hash: - file_hash = file_hash.replace(',{}'.format(self.TMP_FILE_EXT), - '') - - file_info = self.prepare_file_info(path, - integrated_file_sizes[dest], - file_hash, - instance=instance) + file_info = self.prepare_file_info(dest, anatomy, sites=sites) output_resources.append(file_info) return output_resources @@ -1031,8 +972,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dest += '.{}'.format(self.TMP_FILE_EXT) return dest - def prepare_file_info(self, path, size=None, file_hash=None, - sites=None, instance=None): + def get_dest_final_url(self, temp_file_url): + """Temporary destination file url to final destination file url""" + return re.sub(r'\.{}$'.format(self.TMP_FILE_EXT), '', temp_file_url) + + def prepare_file_info(self, path, anatomy, sites): """ Prepare information for one file (asset or resource) Arguments: @@ -1042,74 +986,78 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): sites(optional): array of published locations, [ {'name':'studio', 'created_dt':date} by default keys expected ['studio', 'site1', 'gdrive1'] - instance(dict, optional): to get collected settings Returns: rec: dictionary with filled info """ + file_hash = openpype.api.source_hash(path) + + # todo: Avoid this logic + # Strip the temporary file extension from the file hash + if self.TMP_FILE_EXT and ',{}'.format(self.TMP_FILE_EXT) in file_hash: + file_hash = file_hash.replace(',{}'.format(self.TMP_FILE_EXT), '') + + return { + "_id": io.ObjectId(), + "path": self.get_rootless_path(anatomy, path), + "size": os.path.getsize(path), + "hash": file_hash, + "sites": sites + } + + def compute_resource_sync_sites(self, instance): + """Get available resource sync sites""" + # Sync server logic + # TODO: Clean up sync settings local_site = 'studio' # default remote_site = None - always_accesible = [] + always_accessible = [] sync_project_presets = None - rec = { - "_id": io.ObjectId(), - "path": path - } - if size: - rec["size"] = size + system_sync_server_presets = ( + instance.context.data["system_settings"] + ["modules"] + ["sync_server"]) + log.debug("system_sett:: {}".format(system_sync_server_presets)) - if file_hash: - rec["hash"] = file_hash - - if sites: - rec["sites"] = sites - else: - system_sync_server_presets = ( - instance.context.data["system_settings"] - ["modules"] + if system_sync_server_presets["enabled"]: + sync_project_presets = ( + instance.context.data["project_settings"] + ["global"] ["sync_server"]) - log.debug("system_sett:: {}".format(system_sync_server_presets)) - if system_sync_server_presets["enabled"]: - sync_project_presets = ( - instance.context.data["project_settings"] - ["global"] - ["sync_server"]) + if sync_project_presets and sync_project_presets["enabled"]: + local_site, remote_site = self._get_sites(sync_project_presets) + always_accessible = sync_project_presets["config"]. \ + get("always_accessible_on", []) - if sync_project_presets and sync_project_presets["enabled"]: - local_site, remote_site = self._get_sites(sync_project_presets) + already_attached_sites = {} + meta = {"name": local_site, "created_dt": datetime.now()} + sites = [meta] + already_attached_sites[meta["name"]] = meta["created_dt"] - always_accesible = sync_project_presets["config"]. \ - get("always_accessible_on", []) + if sync_project_presets and sync_project_presets["enabled"]: + if remote_site and \ + remote_site not in already_attached_sites.keys(): + # add remote + meta = {"name": remote_site.strip()} + sites.append(meta) + already_attached_sites[meta["name"]] = None - already_attached_sites = {} - meta = {"name": local_site, "created_dt": datetime.now()} - rec["sites"] = [meta] - already_attached_sites[meta["name"]] = meta["created_dt"] - - if sync_project_presets and sync_project_presets["enabled"]: - if remote_site and \ - remote_site not in already_attached_sites.keys(): - # add remote - meta = {"name": remote_site.strip()} - rec["sites"].append(meta) + # add skeleton for site where it should be always synced to + for always_on_site in always_accessible: + if always_on_site not in already_attached_sites.keys(): + meta = {"name": always_on_site.strip()} + sites.append(meta) already_attached_sites[meta["name"]] = None - # add skeleton for site where it should be always synced to - for always_on_site in always_accesible: - if always_on_site not in already_attached_sites.keys(): - meta = {"name": always_on_site.strip()} - rec["sites"].append(meta) - already_attached_sites[meta["name"]] = None + # add alternative sites + alt = self._add_alternative_sites(system_sync_server_presets, + already_attached_sites) + sites.extend(alt) - # add alternative sites - rec = self._add_alternative_sites(system_sync_server_presets, - already_attached_sites, - rec) + log.debug("final sites:: {}".format(sites)) - log.debug("final sites:: {}".format(rec["sites"])) - - return rec + return sites def _get_sites(self, sync_project_presets): """Returns tuple (local_site, remote_site)""" @@ -1129,14 +1077,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def _add_alternative_sites(self, system_sync_server_presets, - already_attached_sites, - rec): + already_attached_sites): """Loop through all configured sites and add alternatives. See SyncServerModule.handle_alternate_site """ conf_sites = system_sync_server_presets.get("sites", {}) + alternative_sites = [] for site_name, site_info in conf_sites.items(): alt_sites = set(site_info.get("alternative_sites", [])) already_attached_keys = list(already_attached_sites.keys()) @@ -1149,12 +1097,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # alt site inherits state of 'created_dt' if real_created: meta["created_dt"] = real_created - rec["sites"].append(meta) + alternative_sites.append(meta) already_attached_sites[meta["name"]] = real_created - return rec + return alternative_sites - def handle_destination_files(self, integrated_file_sizes, mode): + def handle_destination_files(self, destinations, mode): """ Clean destination files Called when error happened during integrating to DB or to disk OR called to rename uploaded files from temporary name to final to @@ -1162,46 +1110,38 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): Used to clean unwanted files Arguments: - integrated_file_sizes: dictionary, file urls as keys, size as value + destinations (list): file paths mode: 'remove' - clean files, 'finalize' - rename files, remove TMP_FILE_EXT suffix denoting temp file """ - if integrated_file_sizes: - for file_url, _file_size in integrated_file_sizes.items(): - if not os.path.exists(file_url): + if not destinations: + return + + for file_url in destinations: + if not os.path.exists(file_url): + self.log.debug( + "File {} was not found.".format(file_url) + ) + continue + + try: + if mode == 'remove': + self.log.debug("Removing file {}".format(file_url)) + os.remove(file_url) + if mode == 'finalize': + + new_name = self.get_dest_final_url(file_url) + if os.path.exists(new_name): + self.log.debug("Removing existing " + "file: {}".format(new_name)) + os.remove(new_name) + self.log.debug( - "File {} was not found.".format(file_url) + "Renaming file {} to {}".format(file_url, new_name) ) - continue - - try: - if mode == 'remove': - self.log.debug("Removing file {}".format(file_url)) - os.remove(file_url) - if mode == 'finalize': - new_name = re.sub( - r'\.{}$'.format(self.TMP_FILE_EXT), - '', - file_url - ) - - if os.path.exists(new_name): - self.log.debug( - "Overwriting file {} to {}".format( - file_url, new_name - ) - ) - shutil.copy(file_url, new_name) - os.remove(file_url) - else: - self.log.debug( - "Renaming file {} to {}".format( - file_url, new_name - ) - ) - os.rename(file_url, new_name) - except OSError: - self.log.error("Cannot {} file {}".format(mode, file_url), - exc_info=True) - six.reraise(*sys.exc_info()) + os.rename(file_url, new_name) + except OSError: + self.log.error("Cannot {} file {}".format(mode, file_url), + exc_info=True) + six.reraise(*sys.exc_info()) From ae1a9ff4cf996445bd74dcd7641639ed8342592e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 17 Mar 2022 11:49:12 +0100 Subject: [PATCH 002/124] More refactoring + draft (untested) implementation for separating File Transaction logic --- openpype/plugins/publish/integrate_new.py | 421 +++++++++++----------- 1 file changed, 215 insertions(+), 206 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e4986e3b3f..500456eaed 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -1,12 +1,10 @@ import os -from os.path import getsize import logging import sys import copy import clique import errno import six -import re from pymongo import DeleteOne, InsertOne, UpdateOne import pyblish.api @@ -14,7 +12,6 @@ from avalon import io from avalon.api import format_template_with_optional_keys import openpype.api from datetime import datetime -# from pype.modules import ModulesManager from openpype.lib.profiles_filtering import filter_profiles from openpype.lib import ( prepare_template_data, @@ -41,6 +38,160 @@ def get_first_frame_padded(collection): return get_frame_padded(start_frame, padding=collection.padding) +class FileTransaction(object): + """ + + The file transaction is a three step process. + + 1) Rename any existing files to a "temporary backup" during `process()` + 2) Copy the files to final destination during `process()` + 3) Remove any backed up files (*no rollback possible!) during `finalize()` + + Step 3 is done during `finalize()`. If not called the .bak files will + remain on disk. + + These steps try to ensure that we don't overwrite half of any existing + files e.g. if they are currently in use. + + Note: + A regular filesystem is *not* a transactional file system and even + though this implementation tries to produce a 'safe copy' with a + potential rollback do keep in mind that it's inherently unsafe due + to how filesystem works and a myriad of things could happen during + the transaction that break the logic. A file storage could go down, + permissions could be changed, other machines could be moving or writing + files. A lot can happen. + + Warning: + Any folders created during the transfer will not be removed. + + """ + + MODE_COPY = 0 + MODE_HARDLINK = 1 + + def __init__(self, log=None): + + if log is None: + log = logging.getLogger("FileTransaction") + + self.log = log + + # The transfer queue + # todo: make this an actual FIFO queue? + self._transfers = {} + + # Destination file paths that a file was transferred to + self._transferred = [] + + # Backup file location mapping to original locations + self._backup_to_original = {} + + def add(self, src, dst, mode=MODE_COPY): + """Add a new file to transfer queue""" + opts = {"mode": mode} + + src = os.path.normpath(src) + dst = os.path.normpath(dst) + + if dst in self._transfers: + queued_src = self._transfers[dst][0] + if src == queued_src: + self.log.debug("File transfer was already " + "in queue: {} -> {}".format(src, dst)) + return + else: + self.log.warning("File transfer in queue overwritten") + + self._transfers[dst] = (src, opts) + + def process(self): + + # Backup any existing files + for dst in self._transfers.keys(): + if os.path.exists(dst): + # Backup original file + # todo: add timestamp or uuid to ensure unique + backup = dst + ".bak" + self._backup_to_original[backup] = dst + self.log.debug("Backup existing file: " + "{} -> {}".format(dst, backup)) + os.rename(dst, backup) + + # Copy the files to transfer + for dst, (src, opts) in self._transfers.items(): + self._create_folder_for_file(dst) + + if opts["mode"] == self.MODE_COPY: + self.log.debug("Copying file ... {} -> {}".format(src, dst)) + copyfile(src, dst) + elif opts["mode"] == self.MODE_HARDLINK: + self.log.debug("Hardlinking file ... {} -> {}".format(src, dst)) + create_hard_link(src, dst) + + self._transferred.append(dst) + + def finalize(self): + # Delete any backed up files + for backup in self._backup_to_original.keys(): + try: + os.remove(backup) + except OSError: + self.log.error("Failed to remove backup file: " + "{}".format(backup), + exc_info=True) + + def rollback(self): + + errors = 0 + + # Rollback any transferred files + for path in self._transferred: + try: + os.remove(path) + except OSError: + errors += 1 + self.log.error("Failed to rollback created file: " + "{}".format(path), + exc_info=True) + + # Rollback the backups + for backup, original in self._backup_to_original.items(): + try: + os.rename(backup, original) + except OSError: + errors +=1 + self.log.error("Failed to restore original file: " + "{} -> {}".format(backup, original), + exc_info=True) + + if errors: + self.log.error("{} errors occurred during " + "rollback.".format(errors), exc_info=True) + six.reraise(*sys.exc_info()) + + @property + def transferred(self): + """Return the processed transfers destination paths""" + return list(self._transferred) + + @property + def backups(self): + """Return the backup file paths""" + return list(self._backup_to_original.keys()) + + def _create_folder_for_file(self, path): + dirname = os.path.dirname(path) + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + six.reraise(*sys.exc_info()) + + class IntegrateAssetNew(pyblish.api.InstancePlugin): """Resolve any dependency issues @@ -122,18 +273,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ] default_template_name = "publish" - # suffix to denote temporary files, use without '.' - TMP_FILE_EXT = 'tmp' - - # file_url : file_size of all published and uploaded files - destinations = list() - # Attributes set by settings template_name_profiles = None subset_grouping_profiles = None def process(self, instance): - self.destinations = [] # Exclude instances that also contain families from exclude families families = set( @@ -143,17 +287,20 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if families & set(self.exclude_families): return + file_transactions = FileTransaction(log=self.log) try: - self.register(instance) - self.log.info("Integrated Asset in to the database ...") - self.handle_destination_files(self.destinations, - 'finalize') + self.register(instance, file_transactions) except Exception: # clean destination + # todo: rollback any registered entities? (or how safe are we?) + file_transactions.rollback() self.log.critical("Error when registering", exc_info=True) - self.handle_destination_files(self.destinations, 'remove') six.reraise(*sys.exc_info()) + # Finalizing can't be rollbacked safely so no use for moving it to + # the try, except. + file_transactions.finalize() + def prepare_anatomy(self, instance): """Prepare anatomy data used to define representation destinations""" @@ -244,7 +391,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return template_name, anatomy_data - def register(self, instance): + def register(self, instance, file_transactions): instance_stagingdir = instance.data.get("stagingDir") if not instance_stagingdir: @@ -272,9 +419,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): version = self.register_version(instance, subset) instance.data["versionEntity"] = version - instance.data['version'] = version['name'] - existing_repres = list(io.find({ + archived_repres = list(io.find({ "parent": version["_id"], "type": "archived_representation" })) @@ -294,19 +440,47 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): prepared = self.prepare_representation(repre, anatomy_data, template_name, - existing_repres, + archived_repres, version, instance_stagingdir, instance) + representation = prepared["representation"] + + # todo: register the file transfers correctly + for src, dst in representation["transfers"]: + file_transactions.add(src, dst, + mode=file_transactions.MODE_COPY) + for src, dst in representation["hardlinks"]: + file_transactions.add(src, dst, + mode=file_transactions.MODE_HARDLINK) # todo: simplify this? - representation = prepared["representation"] representations.append(representation) published_representations[representation["_id"]] = prepared + # could throw exception, will be caught in 'process' + # all integration to DB is being done together lower, + # so no rollback needed + self.log.debug("Integrating source files to destination ...") + file_transactions.process() + self.log.debug("Backup files " + "{}".format(file_transactions.backups)) + self.log.debug("Integrated files " + "{}".format(file_transactions.transferred)) + + # todo: fix get file info for transferred files per representation + # currently it'd set all files for all representations + # get 'files' info for representation and all attached resources + integrated_files = file_transactions.transferred + self.log.debug("Preparing files information ...") + representation["files"] = self.get_files_info( + instance, + integrated_files + ) + # Remove old representations if there are any (before insertion of new) - if existing_repres: - repre_ids_to_remove = [repre["_id"] for repre in existing_repres] + if archived_repres: + repre_ids_to_remove = [repre["_id"] for repre in archived_repres] io.delete_many({"_id": {"$in": repre_ids_to_remove}}) # Write the new representations to the database @@ -395,7 +569,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def prepare_representation(self, repre, anatomy_data, template_name, - existing_repres, + archived_repres, version, instance_stagingdir, instance): @@ -439,11 +613,13 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.debug("Representation uses instance staging dir: " "{}".format(instance_stagingdir)) stagingdir = instance_stagingdir + if not stagingdir: + raise ValueError("No staging directory set for representation: " + "{}".format(repre)) self.log.debug("Anatomy template name: {}".format(template_name)) anatomy = instance.context.data['anatomy'] - template = os.path.normpath( - anatomy.templates[template_name]["path"]) + template = os.path.normpath(anatomy.templates[template_name]["path"]) is_sequence_representation = isinstance(files, (list, tuple)) if is_sequence_representation: @@ -566,24 +742,21 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): continue repre_context[key] = template_data[key] - # Use previous representation's id if there are any - repre_id = None - repre_name_lower = repre["name"].lower() - for _existing_repre in existing_repres: - # NOTE should we check lowered names? - if repre_name_lower == _existing_repre["name"].lower(): - repre_id = _existing_repre["orig_id"] - break + # Define representation id + repre_id = io.ObjectId() - # Create new id if existing representations does not match - if repre_id is None: - repre_id = io.ObjectId() + # Use previous representation's id if there is a name match + repre_name_lower = repre["name"].lower() + for _archived_repres in archived_repres: + if repre_name_lower == _archived_repres["name"].lower(): + repre_id = _archived_repres["orig_id"] + break # todo: `repre` is not the actual `representation` entity # we should simplify/clarify difference between data above # and the actual representation entity for the database data = repre.get("data") or {} - data.update({'path': dst, 'template': template}) + data.update({'path': repre["published_path"], 'template': template}) representation = { "_id": repre_id, "schema": "openpype:representation-2.0", @@ -597,34 +770,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "context": repre_context } + # todo: simplify/streamline which additional data makes its way into + # the representation context if repre.get("outputName"): representation["context"]["output"] = repre['outputName'] if is_sequence_representation and repre.get("frameStart") is not None: representation['context']['frame'] = template_data["frame"] - # any file that should be physically copied is expected in - # 'transfers' or 'hardlinks' - integrated_files = [] - if instance.data.get('transfers', False) or \ - instance.data.get('hardlinks', False): - # could throw exception, will be caught in 'process' - # all integration to DB is being done together lower, - # so no rollback needed - # todo: separate the actual integrating of the files onto its own - # taking just a list of transfers as inputs (potentially - # with copy mode flag, like hardlink/copy, etc.) - self.log.debug("Integrating source files to destination ...") - integrated_files = self.integrate(instance) - self.log.debug("Integrated files {}".format(integrated_files)) - - # get 'files' info for representation and all attached resources - self.log.debug("Preparing files information ...") - representation["files"] = self.get_files_info( - instance, - integrated_files - ) - return { "representation": representation, "anatomy_data": template_data, @@ -633,84 +786,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "published_files": [transfer[1] for transfer in repre["transfers"]] } - def integrate(self, instance): - """ Move the files. - - Through `instance.data["transfers"]` - - Args: - instance: the instance to integrate - Returns: - list: destination full paths of integrated files - """ - # store destinations for potential rollback and measuring sizes - destinations = [] - transfers = list(instance.data.get("transfers", list())) - for src, dest in transfers: - src = os.path.normpath(src) - dest = os.path.normpath(dest) - if src != dest: - dest = self.get_dest_temp_url(dest) - self.copy_file(src, dest) - destinations.append(dest) - - # Produce hardlinked copies - hardlinks = instance.data.get("hardlinks", list()) - for src, dest in hardlinks: - dest = self.get_dest_temp_url(dest) - if not os.path.exists(dest): - self.hardlink_file(src, dest) - - destinations.append(dest) - - return destinations - - def _create_folder_for_file(self, path): - dirname = os.path.dirname(path) - try: - os.makedirs(dirname) - except OSError as e: - if e.errno == errno.EEXIST: - pass - else: - self.log.critical("An unexpected error occurred.") - six.reraise(*sys.exc_info()) - - def copy_file(self, src, dst): - """Copy source filepath to destination filepath - - Arguments: - src (str): the source file which needs to be copied - dst (str): the destination filepath - - Returns: - None - - """ - self._create_folder_for_file(dst) - self.log.debug("Copying file ... {} -> {}".format(src, dst)) - copyfile(src, dst) - - def hardlink_file(self, src, dst): - """Hardlink source filepath to destination filepath. - - Note: - Hardlink can only be produced between two files on the same - server/disk and editing one of the two will edit both files at - once. As such it is recommended to only make hardlinks between - static files to ensure publishes remain safe and non-edited. - - Arguments: - src (str): the source file which needs to be hardlinked - dst (str): the destination filepath - - Returns: - None - """ - self._create_folder_for_file(dst) - self.log.debug("Hardlinking file ... {} -> {}".format(src, dst)) - create_hard_link(src, dst) - def _get_instance_families(self, instance): """Get all families of the instance""" # todo: move this to lib? @@ -727,7 +802,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def register_subset(self, instance): # todo: rely less on self.prepare_anatomy to create this value - asset = instance.data.get("assetEntity") # <- from prepare_anatomy :( + asset = instance.data.get("assetEntity") # stored by prepare_anatomy subset_name = instance.data["subset"] subset = io.find_one({ "type": "subset", @@ -957,25 +1032,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return output_resources - def get_dest_temp_url(self, dest): - """ Enhance destination path with TMP_FILE_EXT to denote temporary - file. - Temporary files will be renamed after successful registration - into DB and full copy to destination - - Arguments: - dest: destination url of published file (absolute) - Returns: - dest: destination path + '.TMP_FILE_EXT' - """ - if self.TMP_FILE_EXT and '.{}'.format(self.TMP_FILE_EXT) not in dest: - dest += '.{}'.format(self.TMP_FILE_EXT) - return dest - - def get_dest_final_url(self, temp_file_url): - """Temporary destination file url to final destination file url""" - return re.sub(r'\.{}$'.format(self.TMP_FILE_EXT), '', temp_file_url) - def prepare_file_info(self, path, anatomy, sites): """ Prepare information for one file (asset or resource) @@ -991,11 +1047,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): """ file_hash = openpype.api.source_hash(path) - # todo: Avoid this logic - # Strip the temporary file extension from the file hash - if self.TMP_FILE_EXT and ',{}'.format(self.TMP_FILE_EXT) in file_hash: - file_hash = file_hash.replace(',{}'.format(self.TMP_FILE_EXT), '') - return { "_id": io.ObjectId(), "path": self.get_rootless_path(anatomy, path), @@ -1004,6 +1055,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "sites": sites } + # region sync sites def compute_resource_sync_sites(self, instance): """Get available resource sync sites""" # Sync server logic @@ -1101,47 +1153,4 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): already_attached_sites[meta["name"]] = real_created return alternative_sites - - def handle_destination_files(self, destinations, mode): - """ Clean destination files - Called when error happened during integrating to DB or to disk - OR called to rename uploaded files from temporary name to final to - highlight publishing in progress/broken - Used to clean unwanted files - - Arguments: - destinations (list): file paths - mode: 'remove' - clean files, - 'finalize' - rename files, - remove TMP_FILE_EXT suffix denoting temp file - """ - if not destinations: - return - - for file_url in destinations: - if not os.path.exists(file_url): - self.log.debug( - "File {} was not found.".format(file_url) - ) - continue - - try: - if mode == 'remove': - self.log.debug("Removing file {}".format(file_url)) - os.remove(file_url) - if mode == 'finalize': - - new_name = self.get_dest_final_url(file_url) - if os.path.exists(new_name): - self.log.debug("Removing existing " - "file: {}".format(new_name)) - os.remove(new_name) - - self.log.debug( - "Renaming file {} to {}".format(file_url, new_name) - ) - os.rename(file_url, new_name) - except OSError: - self.log.error("Cannot {} file {}".format(mode, file_url), - exc_info=True) - six.reraise(*sys.exc_info()) + # endregion From 9f6cc5df3a11031fb18155c97e0a73bb6f3f6108 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 17 Mar 2022 11:51:06 +0100 Subject: [PATCH 003/124] Fix hound --- openpype/plugins/publish/integrate_new.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 500456eaed..e74b528ae7 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -126,7 +126,8 @@ class FileTransaction(object): self.log.debug("Copying file ... {} -> {}".format(src, dst)) copyfile(src, dst) elif opts["mode"] == self.MODE_HARDLINK: - self.log.debug("Hardlinking file ... {} -> {}".format(src, dst)) + self.log.debug("Hardlinking file ... {} -> {}".format(src, + dst)) create_hard_link(src, dst) self._transferred.append(dst) @@ -160,7 +161,7 @@ class FileTransaction(object): try: os.rename(backup, original) except OSError: - errors +=1 + errors += 1 self.log.error("Failed to restore original file: " "{} -> {}".format(backup, original), exc_info=True) From 56bcd8cec35f201ead80a18c09f6c070b76209c1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 17 Mar 2022 16:30:49 +0100 Subject: [PATCH 004/124] Continue refactor, restore functionality - now can correctly publish as before (rudimentary tested only) --- openpype/plugins/publish/integrate_new.py | 136 +++++++++++----------- 1 file changed, 70 insertions(+), 66 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e74b528ae7..c550c1011c 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -101,7 +101,10 @@ class FileTransaction(object): "in queue: {} -> {}".format(src, dst)) return else: - self.log.warning("File transfer in queue overwritten") + self.log.warning("File transfer in queue replaced..") + self.log.debug("Removed from queue: " + "{} -> {}".format(queued_src, dst)) + self.log.debug("Added to queue: {} -> {}".format(src, dst)) self._transfers[dst] = (src, opts) @@ -298,7 +301,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.critical("Error when registering", exc_info=True) six.reraise(*sys.exc_info()) - # Finalizing can't be rollbacked safely so no use for moving it to + # Finalizing can't rollback safely so no use for moving it to # the try, except. file_transactions.finalize() @@ -426,11 +429,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "type": "archived_representation" })) - # Find the representations to transfer amongst the files - # Each should be a single representation (as such, a single extension) + # Prepare all representations template_name, anatomy_data = self.prepare_anatomy(instance) - published_representations = {} - representations = [] + prepared_representations = [] for repre in instance.data["representations"]: if "delete" in repre.get("tags", []): @@ -438,6 +439,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "{}".format(repre)) continue + # todo: reduce/simplify what is returned from this function prepared = self.prepare_representation(repre, anatomy_data, template_name, @@ -445,23 +447,23 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): version, instance_stagingdir, instance) - representation = prepared["representation"] - # todo: register the file transfers correctly - for src, dst in representation["transfers"]: - file_transactions.add(src, dst, - mode=file_transactions.MODE_COPY) - for src, dst in representation["hardlinks"]: - file_transactions.add(src, dst, - mode=file_transactions.MODE_HARDLINK) + for src, dst in prepared["transfers"]: + # todo: add support for hardlink transfers + file_transactions.add(src, dst) - # todo: simplify this? - representations.append(representation) - published_representations[representation["_id"]] = prepared + prepared_representations.append(prepared) - # could throw exception, will be caught in 'process' - # all integration to DB is being done together lower, - # so no rollback needed + # Each instance can also have pre-defined transfers not explicitly + # part of a representation - like texture resources used by a + # .ma representation. Those destination paths are pre-defined, etc. + # todo: should we move or simplify this logic? + for src, dst in instance.data.get("transfers", []): + file_transactions.add(src, dst, mode=FileTransaction.MODE_COPY) + for src, dst in instance.data.get("hardlinks", []): + file_transactions.add(src, dst, mode=FileTransaction.MODE_HARDLINK) + + # Process all file transfers of all integrations now self.log.debug("Integrating source files to destination ...") file_transactions.process() self.log.debug("Backup files " @@ -469,17 +471,21 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.debug("Integrated files " "{}".format(file_transactions.transferred)) - # todo: fix get file info for transferred files per representation - # currently it'd set all files for all representations - # get 'files' info for representation and all attached resources - integrated_files = file_transactions.transferred - self.log.debug("Preparing files information ...") - representation["files"] = self.get_files_info( - instance, - integrated_files - ) + # Finalize the representations now the published files are integrated + # Get 'files' info for representations and its attached resources + self.log.debug("Retrieving Representation files information ...") + sites = self.compute_resource_sync_sites(instance) + anatomy = instance.context.data["anatomy"] + representations = [] + for prepared in prepared_representations: + transfers = prepared["transfers"] + representation = prepared["representation"] + representation["files"] = self.get_files_info( + transfers, sites, anatomy + ) + representations.append(representation) - # Remove old representations if there are any (before insertion of new) + # Remove all archived representations if archived_repres: repre_ids_to_remove = [repre["_id"] for repre in archived_repres] io.delete_many({"_id": {"$in": repre_ids_to_remove}}) @@ -487,7 +493,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Write the new representations to the database io.insert_many(representations) - instance.data["published_representations"] = published_representations + # Backwards compatibility + # todo: can we avoid the need to store this? + instance.data["published_representations"] = { + p["representation"]["_id"]: p for p in prepared_representations + } self.log.info("Registered {} representations" "".format(len(representations))) @@ -495,7 +505,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def register_version(self, instance, subset): version_number = instance.data["version"] - self.log.debug("Next version: v{}".format(version_number)) + self.log.debug("Version: v{0:03d}".format(version_number)) version_data = self.create_version_data(instance) version_data_instance = instance.data.get('versionData') @@ -565,6 +575,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) version = io.find_one({"_id": version_id}) + + self.log.info("Registered version: v{0:03d}".format(version["name"])) + return version def prepare_representation(self, repre, @@ -585,7 +598,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if repre.get("transfers"): raise ValueError("Representation is not allowed to have transfers" - "data before integration. " + "data before integration. They are computed in " + "the integrator" "Got: {}".format(repre["transfers"])) # required representation keys @@ -698,18 +712,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst_collection.padding = destination_padding assert len(src_collection) == len(dst_collection), "This is a bug" + # Multiple file transfers transfers = [] for src_file_name, dst in zip(src_collection, dst_collection): src = os.path.join(stagingdir, src_file_name) - self.log.debug("source: {}".format(src)) - self.log.debug("destination: `{}`".format(dst)) - transfers.append(src, dst) - - # Store first frame as published path - # todo: remove `published_path` since it can be retrieved from - # `transfers` by taking the first destination transfers[0][1] - repre['published_path'] = next(iter(dst_collection)) - repre["transfers"].extend(transfers) + transfers.append((src, dst)) else: # Single file @@ -728,11 +735,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst = os.path.normpath(template_filled) # Single file transfer - self.log.debug("source: {}".format(src)) - self.log.debug("destination: `{}`".format(dst)) - repre["transfers"] = [src, dst] - - repre['published_path'] = dst + transfers = [(src, dst)] if repre.get("udim"): repre_context["udim"] = repre.get("udim") # store list @@ -753,11 +756,16 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): repre_id = _archived_repres["orig_id"] break + # Backwards compatibility: + # Store first transferred destination as published path data + # todo: can we remove this? + published_path = transfers[0][1] + # todo: `repre` is not the actual `representation` entity # we should simplify/clarify difference between data above # and the actual representation entity for the database data = repre.get("data") or {} - data.update({'path': repre["published_path"], 'template': template}) + data.update({'path': published_path, 'template': template}) representation = { "_id": repre_id, "schema": "openpype:representation-2.0", @@ -782,9 +790,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return { "representation": representation, "anatomy_data": template_data, - # todo: avoid the need for 'published_files'? + "transfers": transfers, + # todo: avoid the need for 'published_files' used by Integrate Hero # backwards compatibility - "published_files": [transfer[1] for transfer in repre["transfers"]] + "published_files": [transfer[1] for transfer in transfers] } def _get_instance_families(self, instance): @@ -805,6 +814,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # todo: rely less on self.prepare_anatomy to create this value asset = instance.data.get("assetEntity") # stored by prepare_anatomy subset_name = instance.data["subset"] + self.log.debug("Subset: {}".format(subset_name)) + subset = io.find_one({ "type": "subset", "parent": asset["_id"], @@ -838,6 +849,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): {"$set": {"data.families": families}} ) + self.log.info("Registered subset: {}".format(subset_name)) + return subset def _set_subset_group(self, instance, subset_id): @@ -871,9 +884,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if not self.subset_grouping_profiles: return None + # TODO: Resolve below questions # QUESTION - # - is there a chance that task name is not filled in anatomy - # data? + # - is there a chance that task name is not filled in anatomy data? # - should we use context task in that case? anatomy_data = instance.data["anatomyData"] task_name = None @@ -1002,7 +1015,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ).format(path)) return path - def get_files_info(self, instance): + def get_files_info(self, transfers, sites, anatomy): """ Prepare 'files' portion for attached resources and main asset. Combining records from 'transfers' and 'hardlinks' parts from instance. @@ -1017,21 +1030,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): output_resources: array of dictionaries to be added to 'files' key in representation """ - # todo: refactor to use transfers/hardlinks of representations - # currently broken logic - resources = list(instance.data.get("transfers", [])) - resources.extend(list(instance.data.get("hardlinks", []))) - self.log.debug("get_files_info.resources:{}".format(resources)) - - sites = self.compute_resource_sync_sites(instance) - - output_resources = [] - anatomy = instance.context.data["anatomy"] - for _src, dest in resources: + file_infos = [] + for _src, dest in transfers: file_info = self.prepare_file_info(dest, anatomy, sites=sites) - output_resources.append(file_info) + file_infos.append(file_info) - return output_resources + return file_infos def prepare_file_info(self, path, anatomy, sites): """ Prepare information for one file (asset or resource) From 8996280224aa30ad800e955ff165bdbe48bb8296 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Mar 2022 23:38:05 +0100 Subject: [PATCH 005/124] Reduce duplicated logic by implementing `resolve_profile` method --- openpype/plugins/publish/integrate_new.py | 107 ++++++++++------------ 1 file changed, 48 insertions(+), 59 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 2142920a09..e43afbf7f6 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -359,17 +359,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "short": task_code } - elif "task" in anatomy_data: - # Just set 'task_name' variable to context task - task_name = anatomy_data["task"]["name"] - task_type = anatomy_data["task"]["type"] - - else: - task_name = None - task_type = None - # Fill family in anatomy data - anatomy_data["family"] = instance.data.get("family") + anatomy_data["family"] = self.main_family_from_instance(instance) intent_value = instance.context.data.get("intent") if intent_value and isinstance(intent_value, dict): @@ -378,25 +369,44 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if intent_value: anatomy_data["intent"] = intent_value - # Get profile - key_values = { - "families": self.main_family_from_instance(instance), - "tasks": task_name, - "hosts": instance.context.data["hostName"], - "task_types": task_type - } - profile = filter_profiles( - self.template_name_profiles, - key_values, - logger=self.log - ) - + profile, _ = self.resolve_profile(self.template_name_profiles, + instance) template_name = "publish" if profile: template_name = profile["template_name"] return template_name, anatomy_data + def resolve_profile(self, profiles, instance): + """Resolve profile by family, task name, host name and task type""" + + # Anatomy data is pre-filled by Collectors and `self.prepare_anatomy` + anatomy_data = instance.data["anatomyData"] + + # TODO: Resolve below questions + # QUESTION + # - is there a chance that task name is not filled in anatomy data? + # - should we use context task in that case? + # Task can be optional in anatomy data + task = anatomy_data.get("task", {}) + + filter_criteria = { + "families": anatomy_data["family"], + "tasks": task.get("name"), + "hosts": anatomy_data["host"], + "task_types": task.get("type") + } + # Get profile + profile = filter_profiles( + profiles, + filter_criteria, + logger=self.log + ) + + # TODO: See if we can simplify to avoid needing to return filter + # criteria used in `self._get_subset_group` + return profile, filter_criteria + def register(self, instance, file_transactions): instance_stagingdir = instance.data.get("stagingDir") @@ -886,50 +896,29 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if not self.subset_grouping_profiles: return None - # TODO: Resolve below questions - # QUESTION - # - is there a chance that task name is not filled in anatomy data? - # - should we use context task in that case? - anatomy_data = instance.data["anatomyData"] - task_name = None - task_type = None - if "task" in anatomy_data: - task_name = anatomy_data["task"]["name"] - task_type = anatomy_data["task"]["type"] - filtering_criteria = { - "families": instance.data["family"], - "hosts": instance.context.data["hostName"], - "tasks": task_name, - "task_types": task_type - } - matching_profile = filter_profiles( - self.subset_grouping_profiles, - filtering_criteria - ) - # Skip if there is not matching profile - if not matching_profile: + # Skip if there is no matching profile + profile, criteria = self.resolve_profile(self.subset_grouping_profiles, + instance) + if not profile: return None - filled_template = None - template = matching_profile["template"] - fill_pairs = ( - ("family", filtering_criteria["families"]), - ("task", filtering_criteria["tasks"]), - ("host", filtering_criteria["hosts"]), - ("subset", instance.data["subset"]), - ("renderlayer", instance.data.get("renderlayer")) - ) - fill_pairs = prepare_template_data(fill_pairs) + template = profile["template"] + fill_pairs = prepare_template_data({ + "family": criteria["families"], + "task": criteria["tasks"], + "host": criteria["hosts"], + "subset": instance.data["subset"], + "renderlayer": instance.data.get("renderlayer") + }) + + filled_template = None try: filled_template = StringTemplate.format_strict_template( template, fill_pairs ) except (KeyError, TemplateUnsolved): - keys = [] - if fill_pairs: - keys = fill_pairs.keys() - + keys = fill_pairs.keys() msg = "Subset grouping failed. " \ "Only {} are expected in Settings".format(','.join(keys)) self.log.warning(msg) From 177e244bd80bf0b1d472948cba45f40dfecd672e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Mar 2022 23:45:24 +0100 Subject: [PATCH 006/124] Remove prepare anatomy data logic that is already collected/generated in CollectAnatomyContextData and CollectAnatomyInstanceData. This currently was duplicated logic and should not be handled in the Integrator --- openpype/plugins/publish/integrate_new.py | 51 +---------------------- 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e43afbf7f6..a1a116bd43 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -310,58 +310,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def prepare_anatomy(self, instance): """Prepare anatomy data used to define representation destinations""" - context = instance.context - anatomy_data = instance.data["anatomyData"] - project_entity = instance.data["projectEntity"] - - context_asset_name = None - context_asset_doc = context.data.get("assetEntity") - if context_asset_doc: - context_asset_name = context_asset_doc["name"] - - asset_name = instance.data["asset"] - asset_entity = instance.data.get("assetEntity") - if not asset_entity or asset_entity["name"] != context_asset_name: - asset_entity = io.find_one({ - "type": "asset", - "name": asset_name, - "parent": project_entity["_id"] - }) - assert asset_entity, ( - "No asset found by the name \"{0}\" in project \"{1}\"" - ).format(asset_name, project_entity["name"]) - - instance.data["assetEntity"] = asset_entity - - # update anatomy data with asset specific keys - # - name should already been set - hierarchy = "" - parents = asset_entity["data"]["parents"] - if parents: - hierarchy = "/".join(parents) - anatomy_data["hierarchy"] = hierarchy - - # Make sure task name in anatomy data is same as on instance.data - asset_tasks = ( - asset_entity.get("data", {}).get("tasks") - ) or {} - task_name = instance.data.get("task") - if task_name: - task_info = asset_tasks.get(task_name) or {} - task_type = task_info.get("type") - - project_task_types = project_entity["config"]["tasks"] - task_code = project_task_types.get(task_type, {}).get("short_name") - anatomy_data["task"] = { - "name": task_name, - "type": task_type, - "short": task_code - } - - # Fill family in anatomy data - anatomy_data["family"] = self.main_family_from_instance(instance) + # TODO: This logic should move to CollectAnatomyContextData intent_value = instance.context.data.get("intent") if intent_value and isinstance(intent_value, dict): intent_value = intent_value.get("value") From 3fd2d020149e5b33c0be0ab7000376a0f30ed96f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Mar 2022 23:55:40 +0100 Subject: [PATCH 007/124] Move logic to clarify what should be removed/moved and bring logic closer to where it's used --- openpype/plugins/publish/integrate_new.py | 36 ++++++++++------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index a1a116bd43..e57fbaf294 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -293,6 +293,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if families & set(self.exclude_families): return + # TODO: Avoid the need to do any adjustments to anatomy data + # Best case scenario that's all handled by collectors + self.prepare_anatomy(instance) + file_transactions = FileTransaction(log=self.log) try: self.register(instance, file_transactions) @@ -309,24 +313,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def prepare_anatomy(self, instance): """Prepare anatomy data used to define representation destinations""" - - anatomy_data = instance.data["anatomyData"] - # TODO: This logic should move to CollectAnatomyContextData intent_value = instance.context.data.get("intent") if intent_value and isinstance(intent_value, dict): intent_value = intent_value.get("value") - - if intent_value: - anatomy_data["intent"] = intent_value - - profile, _ = self.resolve_profile(self.template_name_profiles, - instance) - template_name = "publish" - if profile: - template_name = profile["template_name"] - - return template_name, anatomy_data + if intent_value: + instance.data["anatomyData"]["intent"] = intent_value def resolve_profile(self, profiles, instance): """Resolve profile by family, task name, host name and task type""" @@ -382,6 +374,13 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) ) + # Define publish template name from profiles + profile, _ = self.resolve_profile(self.template_name_profiles, + instance) + template_name = "publish" + if profile: + template_name = profile["template_name"] + subset = self.register_subset(instance) version = self.register_version(instance, subset) @@ -393,7 +392,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): })) # Prepare all representations - template_name, anatomy_data = self.prepare_anatomy(instance) prepared_representations = [] for repre in instance.data["representations"]: @@ -404,7 +402,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # todo: reduce/simplify what is returned from this function prepared = self.prepare_representation(repre, - anatomy_data, template_name, archived_repres, version, @@ -544,16 +541,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return version def prepare_representation(self, repre, - anatomy_data, template_name, archived_repres, version, instance_stagingdir, instance): - # create template data for Anatomy - template_data = copy.deepcopy(anatomy_data) - # pre-flight validations if repre["ext"].startswith("."): raise ValueError("Extension must not start with a dot '.': " @@ -565,6 +558,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "the integrator" "Got: {}".format(repre["transfers"])) + # create template data for Anatomy + template_data = copy.deepcopy(instance.data["anatomyData"]) + # required representation keys files = repre['files'] template_data["representation"] = repre["name"] From 8edfb3f7d3f926539f7f060725b3b7e0b1d697e5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 00:10:59 +0100 Subject: [PATCH 008/124] Simplify profile filtering --- openpype/plugins/publish/integrate_new.py | 42 +++++++++-------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e57fbaf294..bdc045d1db 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -320,35 +320,21 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if intent_value: instance.data["anatomyData"]["intent"] = intent_value - def resolve_profile(self, profiles, instance): - """Resolve profile by family, task name, host name and task type""" - - # Anatomy data is pre-filled by Collectors and `self.prepare_anatomy` + def get_profile_filter_criteria(self, instance): + """Return filter criteria for `filter_profiles`""" + # Anatomy data is pre-filled by Collectors anatomy_data = instance.data["anatomyData"] - # TODO: Resolve below questions - # QUESTION - # - is there a chance that task name is not filled in anatomy data? - # - should we use context task in that case? # Task can be optional in anatomy data task = anatomy_data.get("task", {}) - filter_criteria = { + # Return filter criteria + return { "families": anatomy_data["family"], "tasks": task.get("name"), "hosts": anatomy_data["host"], "task_types": task.get("type") } - # Get profile - profile = filter_profiles( - profiles, - filter_criteria, - logger=self.log - ) - - # TODO: See if we can simplify to avoid needing to return filter - # criteria used in `self._get_subset_group` - return profile, filter_criteria def register(self, instance, file_transactions): @@ -375,8 +361,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) # Define publish template name from profiles - profile, _ = self.resolve_profile(self.template_name_profiles, - instance) + filter_criteria = self.get_profile_filter_criteria(instance) + profile = filter_profiles(self.template_name_profiles, + filter_criteria, + logger=self.log) template_name = "publish" if profile: template_name = profile["template_name"] @@ -844,17 +832,19 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return None # Skip if there is no matching profile - profile, criteria = self.resolve_profile(self.subset_grouping_profiles, - instance) + filter_criteria = self.get_profile_filter_criteria(instance) + profile = filter_profiles(self.subset_grouping_profiles, + filter_criteria, + logger=self.log) if not profile: return None template = profile["template"] fill_pairs = prepare_template_data({ - "family": criteria["families"], - "task": criteria["tasks"], - "host": criteria["hosts"], + "family": filter_criteria["families"], + "task": filter_criteria["tasks"], + "host": filter_criteria["hosts"], "subset": instance.data["subset"], "renderlayer": instance.data.get("renderlayer") }) From 79286ead4b91504afa30df711e8f751451f53552 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 00:16:32 +0100 Subject: [PATCH 009/124] Re-use get families logic --- openpype/plugins/publish/integrate_new.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index bdc045d1db..e66a71c483 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -286,10 +286,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def process(self, instance): # Exclude instances that also contain families from exclude families - families = set( - # Consider family and families data - [instance.data["family"]] + instance.data.get("families", []) - ) + families = set(self._get_instance_families(instance)) if families & set(self.exclude_families): return From d6c682723de6eb025b21768ced54b2756373fba6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 00:19:16 +0100 Subject: [PATCH 010/124] Remove todo since assetEntity already comes from Collectors + re-use families variable --- openpype/plugins/publish/integrate_new.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e66a71c483..856f8af163 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -755,8 +755,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return families def register_subset(self, instance): - # todo: rely less on self.prepare_anatomy to create this value - asset = instance.data.get("assetEntity") # stored by prepare_anatomy + asset = instance.data.get("assetEntity") subset_name = instance.data["subset"] self.log.debug("Subset: {}".format(subset_name)) @@ -766,9 +765,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "name": subset_name }) + families = self._get_instance_families(instance) if subset is None: self.log.info("Subset '%s' not found, creating ..." % subset_name) - families = self._get_instance_families(instance) _id = io.insert_one({ "schema": "openpype:subset-3.0", @@ -786,8 +785,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self._set_subset_group(instance, subset["_id"]) # Update families on subset. - families = [instance.data["family"]] - families.extend(instance.data.get("families", [])) io.update_many( {"type": "subset", "_id": ObjectId(subset["_id"])}, {"$set": {"data.families": families}} From 47259f8ef7b177892c76a8dbfde6d147cf664d39 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 00:21:44 +0100 Subject: [PATCH 011/124] Add todo to move get subset group logic --- openpype/plugins/publish/integrate_new.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 856f8af163..91d2f3a943 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -821,6 +821,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): Attribute 'subset_grouping_profiles' is defined by OpenPype settings. """ + # TODO: This logic is better suited for a Collector to just store + # instance.data["subsetGroup"] # Skip if 'subset_grouping_profiles' is empty if not self.subset_grouping_profiles: return None From b128e0addffc77991e5ff25f2d219d8ed8613136 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 14:21:32 +0100 Subject: [PATCH 012/124] Override stored repre context `udim` for backwards compatibility --- openpype/plugins/publish/integrate_new.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 91d2f3a943..e3abb8f04f 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -275,7 +275,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): exclude_families = ["clip"] db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", - "family", "hierarchy", "task", "username", "frame", "udim" + "family", "hierarchy", "task", "username", "frame" ] default_template_name = "publish" @@ -681,15 +681,17 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Single file transfer transfers = [(src, dst)] - if repre.get("udim"): - repre_context["udim"] = repre.get("udim") # store list - for key in self.db_representation_context_keys: value = template_data.get(key) if not value: continue repre_context[key] = template_data[key] + # Explicitly store the full list even though template data might + # have a different value + if repre.get("udim"): + repre_context["udim"] = repre.get("udim") # store list + # Define representation id repre_id = ObjectId() From 9997acbbeae32f1473c39df6cf78a8bfa7257aff Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 14:22:49 +0100 Subject: [PATCH 013/124] Encapsulate version data completely into its own function --- openpype/plugins/publish/integrate_new.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e3abb8f04f..6e92f81b8b 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -452,17 +452,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): version_number = instance.data["version"] self.log.debug("Version: v{0:03d}".format(version_number)) - version_data = self.create_version_data(instance) - version_data_instance = instance.data.get('versionData') - if version_data_instance: - version_data.update(version_data_instance) - version = { "schema": "openpype:version-3.0", "type": "version", "parent": subset["_id"], "name": version_number, - "data": version_data + "data": self.create_version_data(instance) } repres = instance.data.get("representations", []) @@ -909,6 +904,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if key in instance.data: version_data[key] = instance.data[key] + # Include instance.data[versionData] directly + version_data_instance = instance.data.get('versionData') + if version_data_instance: + version_data.update(version_data_instance) + return version_data def main_family_from_instance(self, instance): From 5b1f6eb30c459011fa685dcf325f39c4af72838e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 14:23:27 +0100 Subject: [PATCH 014/124] Move logic closer to where it's used --- openpype/plugins/publish/integrate_new.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 6e92f81b8b..a787f8d50d 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -460,9 +460,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "data": self.create_version_data(instance) } - repres = instance.data.get("representations", []) - new_repre_names_low = [_repre["name"].lower() for _repre in repres] - existing_version = io.find_one({ 'type': 'version', 'parent': subset["_id"], @@ -488,6 +485,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): })) # Find representations of existing version and archive them + repres = instance.data.get("representations", []) + new_repre_names_low = [_repre["name"].lower() for _repre in repres] current_repres = io.find({ "type": "representation", "parent": version_id From 3369c15bdf837d6d8e83a8c054794e95fccd061b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 14:25:15 +0100 Subject: [PATCH 015/124] Preparation to delay Version document write to database closer to representation write --- openpype/plugins/publish/integrate_new.py | 24 ++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index a787f8d50d..8dd2d57959 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -466,15 +466,16 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): 'name': version_number }) + bulk_writes = [] if existing_version is None: self.log.debug("Creating new version ...") - version_id = io.insert_one(version).inserted_id + version["_id"] = ObjectId() + bulk_writes.append(InsertOne(version)) else: self.log.debug("Updating existing version ...") # Check if instance have set `append` mode which cause that # only replicated representations are set to archive append_repres = instance.data.get("append", False) - bulk_writes = [] # Update version data version_id = existing_version['_id'] @@ -484,6 +485,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): '$set': version })) + # Instead of directly writing and querying we reproduce what + # the resulting version would look like so we can hold off making + # changes to the database to avoid the need for 'rollback' + version = copy.deepcopy(version) + version["_id"] = existing_version["_id"] + # Find representations of existing version and archive them repres = instance.data.get("representations", []) new_repre_names_low = [_repre["name"].lower() for _repre in repres] @@ -507,13 +514,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): repre["type"] = "archived_representation" bulk_writes.append(InsertOne(repre)) - # bulk updates - if bulk_writes: - io._database[io.Session["AVALON_PROJECT"]].bulk_write( - bulk_writes - ) - - version = io.find_one({"_id": version_id}) + # bulk updates + # todo: Try to avoid writing already until after we've prepared + # representations to allow easier rollback? + io._database[io.Session["AVALON_PROJECT"]].bulk_write( + bulk_writes + ) self.log.info("Registered version: v{0:03d}".format(version["name"])) From 42175ff6f829ce30ef61538243d7bd4b804c8e28 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 14:41:56 +0100 Subject: [PATCH 016/124] Fix `get_profile_filter_criteria` anatomy data key for app name --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 8dd2d57959..e3dcfcc93c 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -329,7 +329,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return { "families": anatomy_data["family"], "tasks": task.get("name"), - "hosts": anatomy_data["host"], + "hosts": anatomy_data["app"], "task_types": task.get("type") } From 7713af5a1dac4b0080dc6006f08811dcd9fc9d04 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 17:23:02 +0100 Subject: [PATCH 017/124] Fix sequence functionality --- openpype/plugins/publish/integrate_new.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e3dcfcc93c..b5986a62ee 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -645,16 +645,20 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): repre_context = template_filled.used_values self.log.debug("Template filled: {}".format(str(template_filled))) dst_collections, _remainder = clique.assemble( - [os.path.normpath(template_filled)], minimum_items=1 + [os.path.normpath(template_filled)], + minimum_items=1, + patterns=[clique.PATTERNS["frames"]] ) assert not _remainder, "This is a bug" assert len(dst_collections) == 1, "This is a bug" dst_collection = dst_collections[0] # Update the destination indexes and padding - dst_collection.indexes = destination_indexes + dst_collection.indexes.clear() + dst_collection.indexes.update(set(destination_indexes)) dst_collection.padding = destination_padding - assert len(src_collection) == len(dst_collection), "This is a bug" + assert len(src_collection.indexes) == \ + len(dst_collection.indexes), "This is a bug" # Multiple file transfers transfers = [] From 229626bffdbc7e59c2206798b5bb3066a5602228 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 17:36:01 +0100 Subject: [PATCH 018/124] Reformat code --- openpype/plugins/publish/integrate_new.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index b5986a62ee..9e3e9de77c 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -657,8 +657,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst_collection.indexes.clear() dst_collection.indexes.update(set(destination_indexes)) dst_collection.padding = destination_padding - assert len(src_collection.indexes) == \ - len(dst_collection.indexes), "This is a bug" + assert ( + len(src_collection.indexes) == len(dst_collection.indexes) + ), "This is a bug" # Multiple file transfers transfers = [] From e1eb0887e0bdaaf012e95f289bdaddcf9089d65c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Mar 2022 20:26:10 +0100 Subject: [PATCH 019/124] Reduce database calls for register subset + prepare for bulk writes logic --- openpype/plugins/publish/integrate_new.py | 72 ++++++++++------------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 9e3e9de77c..44768df368 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -766,63 +766,53 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): subset_name = instance.data["subset"] self.log.debug("Subset: {}".format(subset_name)) + # Get existing subset if it exists subset = io.find_one({ "type": "subset", "parent": asset["_id"], "name": subset_name }) - families = self._get_instance_families(instance) - if subset is None: - self.log.info("Subset '%s' not found, creating ..." % subset_name) + # Define subset data + data = { + "families": self._get_instance_families(instance) + } - _id = io.insert_one({ + subset_group = instance.data.get("subsetGroup") + if not subset_group: + # todo: move _get_subset_group fallback to its own collector + subset_group = self._get_subset_group(instance) + if subset_group: + data["subsetGroup"] = subset_group + + if subset is None: + # Create a new subset + self.log.info("Subset '%s' not found, creating ..." % subset_name) + subset = { + "_id": ObjectId(), "schema": "openpype:subset-3.0", "type": "subset", "name": subset_name, - "data": { - "families": families - }, + "data": data, "parent": asset["_id"] - }).inserted_id + } + io.insert_one(subset) - subset = io.find_one({"_id": _id}) - - # Update subset group - self._set_subset_group(instance, subset["_id"]) - - # Update families on subset. - io.update_many( - {"type": "subset", "_id": ObjectId(subset["_id"])}, - {"$set": {"data.families": families}} - ) + else: + # Update existing subset data with new data and set in database. + # We also change the found subset in-place so we don't need to + # re-query the subset afterwards + subset["data"].update(data) + io.update_many( + {"type": "subset", "_id": subset["_id"]}, + {"$set": { + "data": subset["data"] + }} + ) self.log.info("Registered subset: {}".format(subset_name)) - return subset - def _set_subset_group(self, instance, subset_id): - """ - Mark subset as belonging to group in DB. - - Uses Settings > Global > Publish plugins > IntegrateAssetNew - - Args: - instance (dict): processed instance - subset_id (str): DB's subset _id - - """ - # Fist look into instance data - subset_group = instance.data.get("subsetGroup") - if not subset_group: - subset_group = self._get_subset_group(instance) - - if subset_group: - io.update_many({ - 'type': 'subset', - '_id': ObjectId(subset_id) - }, {'$set': {'data.subsetGroup': subset_group}}) - def _get_subset_group(self, instance): """Look into subset group profiles set by settings. From b906365f593025bf7bbba67ea6d8a907b717c98e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 25 Mar 2022 21:42:39 +0100 Subject: [PATCH 020/124] Separate site sync logic further from Integrator plug-in (Draft) --- openpype/plugins/publish/integrate_new.py | 154 ++++++++++++---------- 1 file changed, 88 insertions(+), 66 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 44768df368..138a4fcc06 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -419,7 +419,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Finalize the representations now the published files are integrated # Get 'files' info for representations and its attached resources self.log.debug("Retrieving Representation files information ...") - sites = self.compute_resource_sync_sites(instance) + sites = SiteSync.compute_resource_sync_sites( + system_settings=instance.context.data["system_settings"], + project_settings=instance.context.data["project_settings"] + ) + log.debug("final sites:: {}".format(sites)) + anatomy = instance.context.data["anatomy"] representations = [] for prepared in prepared_representations: @@ -987,63 +992,65 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "sites": sites } - # region sync sites - def compute_resource_sync_sites(self, instance): + +class SiteSync(object): + """Logic for Site Sync Module functionality""" + + @classmethod + def compute_resource_sync_sites(cls, + system_settings, + project_settings): """Get available resource sync sites""" - # Sync server logic - # TODO: Clean up sync settings - local_site = 'studio' # default - remote_site = None - always_accessible = [] - sync_project_presets = None - system_sync_server_presets = ( - instance.context.data["system_settings"] - ["modules"] - ["sync_server"]) + def create_metadata(name, created=True): + """Create sync site metadata for site with `name`""" + metadata = {"name": name} + if created: + metadata["created_dt"] = datetime.now() + return metadata + + default_sites = [create_metadata("studio")] + + # If sync site module is disabled return default fallback site + system_sync_server_presets = system_settings["modules"]["sync_server"] log.debug("system_sett:: {}".format(system_sync_server_presets)) + if not system_sync_server_presets["enabled"]: + return default_sites - if system_sync_server_presets["enabled"]: - sync_project_presets = ( - instance.context.data["project_settings"] - ["global"] - ["sync_server"]) + # If sync site module is disabled in current + # project return default fallback site + sync_project_presets = project_settings["global"]["sync_server"] + if not sync_project_presets["enabled"]: + return default_sites - if sync_project_presets and sync_project_presets["enabled"]: - local_site, remote_site = self._get_sites(sync_project_presets) - always_accessible = sync_project_presets["config"]. \ - get("always_accessible_on", []) + local_site, remote_site = cls._get_sites(sync_project_presets) - already_attached_sites = {} - meta = {"name": local_site, "created_dt": datetime.now()} - sites = [meta] - already_attached_sites[meta["name"]] = meta["created_dt"] + # Attached sites metadata by site name + # That is the local site, remote site, the always accesible sites + # and their alternate sites (alias of sites with different protocol) + attached_sites = dict() + attached_sites[local_site] = create_metadata(local_site) - if sync_project_presets and sync_project_presets["enabled"]: - if remote_site and \ - remote_site not in already_attached_sites.keys(): - # add remote - meta = {"name": remote_site.strip()} - sites.append(meta) - already_attached_sites[meta["name"]] = None + if remote_site and remote_site != local_site: + attached_sites[remote_site] = create_metadata(remote_site, + created=False) - # add skeleton for site where it should be always synced to - for always_on_site in always_accessible: - if always_on_site not in already_attached_sites.keys(): - meta = {"name": always_on_site.strip()} - sites.append(meta) - already_attached_sites[meta["name"]] = None + # add skeleton for sites where it should be always synced to + always_accessible_sites = ( + sync_project_presets["config"].get("always_accessible_on", []) + ) + for site in always_accessible_sites: + site = site.strip() + if site not in attached_sites: + attached_sites[site] = create_metadata(site, created=False) - # add alternative sites - alt = self._add_alternative_sites(system_sync_server_presets, - already_attached_sites) - sites.extend(alt) + # add alternative sites + cls._add_alternative_sites(system_sync_server_presets, attached_sites) - log.debug("final sites:: {}".format(sites)) + return list(attached_sites.values()) - return sites - - def _get_sites(self, sync_project_presets): + @staticmethod + def _get_sites(sync_project_presets): """Returns tuple (local_site, remote_site)""" local_site_id = openpype.api.get_local_site_id() local_site = sync_project_presets["config"]. \ @@ -1053,36 +1060,51 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): local_site = local_site_id remote_site = sync_project_presets["config"].get("remote_site") + if remote_site: + remote_site.strip() if remote_site == 'local': remote_site = local_site_id return local_site, remote_site - def _add_alternative_sites(self, - system_sync_server_presets, - already_attached_sites): + @staticmethod + def _add_alternative_sites(system_sync_server_presets, + attached_sites): """Loop through all configured sites and add alternatives. + For all sites if an alternative site is detected that has an + accessible site then we can also register to that alternative site + with the same "created" state. So we match the existing data. + See SyncServerModule.handle_alternate_site """ conf_sites = system_sync_server_presets.get("sites", {}) - alternative_sites = [] for site_name, site_info in conf_sites.items(): - alt_sites = set(site_info.get("alternative_sites", [])) - already_attached_keys = list(already_attached_sites.keys()) - for added_site in already_attached_keys: - if added_site in alt_sites: - if site_name in already_attached_keys: - continue - meta = {"name": site_name} - real_created = already_attached_sites[added_site] - # alt site inherits state of 'created_dt' - if real_created: - meta["created_dt"] = real_created - alternative_sites.append(meta) - already_attached_sites[meta["name"]] = real_created - return alternative_sites - # endregion + # Skip if already defined + if site_name in attached_sites: + continue + + # Get alternate sites (stripped names) for this site name + alt_sites = site_info.get("alternative_sites", []) + alt_sites = [site.strip() for site in alt_sites] + alt_sites = set(alt_sites) + + # If no alternative sites we don't need to add + if not alt_sites: + continue + + # Take a copy of data of the first alternate site that is already + # defined as an attached site to match the same state. + match_meta = next((attached_sites[site] for site in alt_sites + if site in attached_sites), None) + if not match_meta: + continue + + alt_site_meta = copy.deepcopy(match_meta) + alt_site_meta["name"] = site_name + + # Note: We change mutable `attached_site` dict in-place + attached_sites[site_name] = alt_site_meta From e0aaa5f6cc2fd2a2e6fa708364136d9d6235163d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 14:20:13 +0100 Subject: [PATCH 021/124] Move FileTransaction into lib --- openpype/lib/file_transaction.py | 171 ++++++++++++++++++++++ openpype/plugins/publish/integrate_new.py | 167 +-------------------- 2 files changed, 172 insertions(+), 166 deletions(-) create mode 100644 openpype/lib/file_transaction.py diff --git a/openpype/lib/file_transaction.py b/openpype/lib/file_transaction.py new file mode 100644 index 0000000000..57592e297f --- /dev/null +++ b/openpype/lib/file_transaction.py @@ -0,0 +1,171 @@ +import os +import logging +import sys +import errno +import six + +from openpype.lib import create_hard_link + +# this is needed until speedcopy for linux is fixed +if sys.platform == "win32": + from speedcopy import copyfile +else: + from shutil import copyfile + + +class FileTransaction(object): + """ + + The file transaction is a three step process. + + 1) Rename any existing files to a "temporary backup" during `process()` + 2) Copy the files to final destination during `process()` + 3) Remove any backed up files (*no rollback possible!) during `finalize()` + + Step 3 is done during `finalize()`. If not called the .bak files will + remain on disk. + + These steps try to ensure that we don't overwrite half of any existing + files e.g. if they are currently in use. + + Note: + A regular filesystem is *not* a transactional file system and even + though this implementation tries to produce a 'safe copy' with a + potential rollback do keep in mind that it's inherently unsafe due + to how filesystem works and a myriad of things could happen during + the transaction that break the logic. A file storage could go down, + permissions could be changed, other machines could be moving or writing + files. A lot can happen. + + Warning: + Any folders created during the transfer will not be removed. + + """ + + MODE_COPY = 0 + MODE_HARDLINK = 1 + + def __init__(self, log=None): + + if log is None: + log = logging.getLogger("FileTransaction") + + self.log = log + + # The transfer queue + # todo: make this an actual FIFO queue? + self._transfers = {} + + # Destination file paths that a file was transferred to + self._transferred = [] + + # Backup file location mapping to original locations + self._backup_to_original = {} + + def add(self, src, dst, mode=MODE_COPY): + """Add a new file to transfer queue""" + opts = {"mode": mode} + + src = os.path.normpath(src) + dst = os.path.normpath(dst) + + if dst in self._transfers: + queued_src = self._transfers[dst][0] + if src == queued_src: + self.log.debug("File transfer was already " + "in queue: {} -> {}".format(src, dst)) + return + else: + self.log.warning("File transfer in queue replaced..") + self.log.debug("Removed from queue: " + "{} -> {}".format(queued_src, dst)) + self.log.debug("Added to queue: {} -> {}".format(src, dst)) + + self._transfers[dst] = (src, opts) + + def process(self): + + # Backup any existing files + for dst in self._transfers.keys(): + if os.path.exists(dst): + # Backup original file + # todo: add timestamp or uuid to ensure unique + backup = dst + ".bak" + self._backup_to_original[backup] = dst + self.log.debug("Backup existing file: " + "{} -> {}".format(dst, backup)) + os.rename(dst, backup) + + # Copy the files to transfer + for dst, (src, opts) in self._transfers.items(): + self._create_folder_for_file(dst) + + if opts["mode"] == self.MODE_COPY: + self.log.debug("Copying file ... {} -> {}".format(src, dst)) + copyfile(src, dst) + elif opts["mode"] == self.MODE_HARDLINK: + self.log.debug("Hardlinking file ... {} -> {}".format(src, + dst)) + create_hard_link(src, dst) + + self._transferred.append(dst) + + def finalize(self): + # Delete any backed up files + for backup in self._backup_to_original.keys(): + try: + os.remove(backup) + except OSError: + self.log.error("Failed to remove backup file: " + "{}".format(backup), + exc_info=True) + + def rollback(self): + + errors = 0 + + # Rollback any transferred files + for path in self._transferred: + try: + os.remove(path) + except OSError: + errors += 1 + self.log.error("Failed to rollback created file: " + "{}".format(path), + exc_info=True) + + # Rollback the backups + for backup, original in self._backup_to_original.items(): + try: + os.rename(backup, original) + except OSError: + errors += 1 + self.log.error("Failed to restore original file: " + "{} -> {}".format(backup, original), + exc_info=True) + + if errors: + self.log.error("{} errors occurred during " + "rollback.".format(errors), exc_info=True) + six.reraise(*sys.exc_info()) + + @property + def transferred(self): + """Return the processed transfers destination paths""" + return list(self._transferred) + + @property + def backups(self): + """Return the backup file paths""" + return list(self._backup_to_original.keys()) + + def _create_folder_for_file(self, path): + dirname = os.path.dirname(path) + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + six.reraise(*sys.exc_info()) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 138a4fcc06..92976e6151 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -3,7 +3,6 @@ import logging import sys import copy import clique -import errno import six from bson.objectid import ObjectId @@ -13,19 +12,13 @@ from avalon import io import openpype.api from datetime import datetime from openpype.lib.profiles_filtering import filter_profiles +from openpype.lib.file_transaction import FileTransaction from openpype.lib import ( prepare_template_data, - create_hard_link, StringTemplate, TemplateUnsolved ) -# this is needed until speedcopy for linux is fixed -if sys.platform == "win32": - from speedcopy import copyfile -else: - from shutil import copyfile - log = logging.getLogger(__name__) @@ -40,164 +33,6 @@ def get_first_frame_padded(collection): return get_frame_padded(start_frame, padding=collection.padding) -class FileTransaction(object): - """ - - The file transaction is a three step process. - - 1) Rename any existing files to a "temporary backup" during `process()` - 2) Copy the files to final destination during `process()` - 3) Remove any backed up files (*no rollback possible!) during `finalize()` - - Step 3 is done during `finalize()`. If not called the .bak files will - remain on disk. - - These steps try to ensure that we don't overwrite half of any existing - files e.g. if they are currently in use. - - Note: - A regular filesystem is *not* a transactional file system and even - though this implementation tries to produce a 'safe copy' with a - potential rollback do keep in mind that it's inherently unsafe due - to how filesystem works and a myriad of things could happen during - the transaction that break the logic. A file storage could go down, - permissions could be changed, other machines could be moving or writing - files. A lot can happen. - - Warning: - Any folders created during the transfer will not be removed. - - """ - - MODE_COPY = 0 - MODE_HARDLINK = 1 - - def __init__(self, log=None): - - if log is None: - log = logging.getLogger("FileTransaction") - - self.log = log - - # The transfer queue - # todo: make this an actual FIFO queue? - self._transfers = {} - - # Destination file paths that a file was transferred to - self._transferred = [] - - # Backup file location mapping to original locations - self._backup_to_original = {} - - def add(self, src, dst, mode=MODE_COPY): - """Add a new file to transfer queue""" - opts = {"mode": mode} - - src = os.path.normpath(src) - dst = os.path.normpath(dst) - - if dst in self._transfers: - queued_src = self._transfers[dst][0] - if src == queued_src: - self.log.debug("File transfer was already " - "in queue: {} -> {}".format(src, dst)) - return - else: - self.log.warning("File transfer in queue replaced..") - self.log.debug("Removed from queue: " - "{} -> {}".format(queued_src, dst)) - self.log.debug("Added to queue: {} -> {}".format(src, dst)) - - self._transfers[dst] = (src, opts) - - def process(self): - - # Backup any existing files - for dst in self._transfers.keys(): - if os.path.exists(dst): - # Backup original file - # todo: add timestamp or uuid to ensure unique - backup = dst + ".bak" - self._backup_to_original[backup] = dst - self.log.debug("Backup existing file: " - "{} -> {}".format(dst, backup)) - os.rename(dst, backup) - - # Copy the files to transfer - for dst, (src, opts) in self._transfers.items(): - self._create_folder_for_file(dst) - - if opts["mode"] == self.MODE_COPY: - self.log.debug("Copying file ... {} -> {}".format(src, dst)) - copyfile(src, dst) - elif opts["mode"] == self.MODE_HARDLINK: - self.log.debug("Hardlinking file ... {} -> {}".format(src, - dst)) - create_hard_link(src, dst) - - self._transferred.append(dst) - - def finalize(self): - # Delete any backed up files - for backup in self._backup_to_original.keys(): - try: - os.remove(backup) - except OSError: - self.log.error("Failed to remove backup file: " - "{}".format(backup), - exc_info=True) - - def rollback(self): - - errors = 0 - - # Rollback any transferred files - for path in self._transferred: - try: - os.remove(path) - except OSError: - errors += 1 - self.log.error("Failed to rollback created file: " - "{}".format(path), - exc_info=True) - - # Rollback the backups - for backup, original in self._backup_to_original.items(): - try: - os.rename(backup, original) - except OSError: - errors += 1 - self.log.error("Failed to restore original file: " - "{} -> {}".format(backup, original), - exc_info=True) - - if errors: - self.log.error("{} errors occurred during " - "rollback.".format(errors), exc_info=True) - six.reraise(*sys.exc_info()) - - @property - def transferred(self): - """Return the processed transfers destination paths""" - return list(self._transferred) - - @property - def backups(self): - """Return the backup file paths""" - return list(self._backup_to_original.keys()) - - def _create_folder_for_file(self, path): - dirname = os.path.dirname(path) - try: - os.makedirs(dirname) - except OSError as e: - if e.errno == errno.EEXIST: - pass - else: - self.log.critical("An unexpected error occurred.") - six.reraise(*sys.exc_info()) - - class IntegrateAssetNew(pyblish.api.InstancePlugin): """Resolve any dependency issues From d3cb32ebe1df79408ff03fddef4d74a55fa1f4b6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 14:32:34 +0100 Subject: [PATCH 022/124] Collect subset group in a Collector instead of during Integrator --- .../plugins/publish/collect_subset_group.py | 100 ++++++++++++++++++ openpype/plugins/publish/integrate_new.py | 50 --------- 2 files changed, 100 insertions(+), 50 deletions(-) create mode 100644 openpype/plugins/publish/collect_subset_group.py diff --git a/openpype/plugins/publish/collect_subset_group.py b/openpype/plugins/publish/collect_subset_group.py new file mode 100644 index 0000000000..60c1c04e70 --- /dev/null +++ b/openpype/plugins/publish/collect_subset_group.py @@ -0,0 +1,100 @@ +"""Produces instance.data["subsetGroup"] data used during integration. + +Requires: + dict -> context["anatomyData"] *(pyblish.api.CollectorOrder + 0.49) + +Provides: + instance -> subsetGroup (str) + +""" +import pyblish.api + +from openpype.lib.profiles_filtering import filter_profiles +from openpype.lib import ( + prepare_template_data, + StringTemplate, + TemplateUnsolved +) + + +class CollectSubsetGroup(pyblish.api.ContextPlugin): + """Collect Subset Group for publish.""" + + # Run after CollectAnatomyInstanceData + order = pyblish.api.CollectorOrder + 0.495 + label = "Collect Subset Group" + + def process(self, instance): + """Look into subset group profiles set by settings. + + Attribute 'subset_grouping_profiles' is defined by OpenPype settings. + """ + + # TODO: Move this setting to this Collector instead of Integrator + project_settings = instance.context.data["project_settings"] + subset_grouping_profiles = ( + project_settings["global"] + ["publish"] + ["IntegrateAssetNew"] + ["subset_grouping_profiles"] + ) + + # Skip if 'subset_grouping_profiles' is empty + if not subset_grouping_profiles: + return + + # Skip if there is no matching profile + filter_criteria = self.get_profile_filter_criteria(instance) + profile = filter_profiles(subset_grouping_profiles, + filter_criteria, + logger=self.log) + if not profile: + return + + if instance.data.get("subsetGroup"): + # If subsetGroup is already set then allow that value to remain + self.log.debug("Skipping collect subset group due to existing " + "value: {}".format(instance.data["subsetGroup"])) + return + + template = profile["template"] + + fill_pairs = prepare_template_data({ + "family": filter_criteria["families"], + "task": filter_criteria["tasks"], + "host": filter_criteria["hosts"], + "subset": instance.data["subset"], + "renderlayer": instance.data.get("renderlayer") + }) + + filled_template = None + try: + filled_template = StringTemplate.format_strict_template( + template, fill_pairs + ) + except (KeyError, TemplateUnsolved): + keys = fill_pairs.keys() + msg = "Subset grouping failed. " \ + "Only {} are expected in Settings".format(','.join(keys)) + self.log.warning(msg) + + if filled_template: + instance.data["subsetGroup"] = filled_template + + def get_profile_filter_criteria(self, instance): + """Return filter criteria for `filter_profiles`""" + # TODO: This logic is used in much more plug-ins in one way or another + # Maybe better suited for lib? + # Anatomy data is pre-filled by Collectors + anatomy_data = instance.data["anatomyData"] + + # Task can be optional in anatomy data + task = anatomy_data.get("task", {}) + + # Return filter criteria + return { + "families": anatomy_data["family"], + "tasks": task.get("name"), + "hosts": anatomy_data["app"], + "task_types": task.get("type") + } diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 92976e6151..284e110916 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -13,11 +13,6 @@ import openpype.api from datetime import datetime from openpype.lib.profiles_filtering import filter_profiles from openpype.lib.file_transaction import FileTransaction -from openpype.lib import ( - prepare_template_data, - StringTemplate, - TemplateUnsolved -) log = logging.getLogger(__name__) @@ -619,9 +614,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): } subset_group = instance.data.get("subsetGroup") - if not subset_group: - # todo: move _get_subset_group fallback to its own collector - subset_group = self._get_subset_group(instance) if subset_group: data["subsetGroup"] = subset_group @@ -653,48 +645,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.info("Registered subset: {}".format(subset_name)) return subset - def _get_subset_group(self, instance): - """Look into subset group profiles set by settings. - - Attribute 'subset_grouping_profiles' is defined by OpenPype settings. - """ - # TODO: This logic is better suited for a Collector to just store - # instance.data["subsetGroup"] - # Skip if 'subset_grouping_profiles' is empty - if not self.subset_grouping_profiles: - return None - - # Skip if there is no matching profile - filter_criteria = self.get_profile_filter_criteria(instance) - profile = filter_profiles(self.subset_grouping_profiles, - filter_criteria, - logger=self.log) - if not profile: - return None - - template = profile["template"] - - fill_pairs = prepare_template_data({ - "family": filter_criteria["families"], - "task": filter_criteria["tasks"], - "host": filter_criteria["hosts"], - "subset": instance.data["subset"], - "renderlayer": instance.data.get("renderlayer") - }) - - filled_template = None - try: - filled_template = StringTemplate.format_strict_template( - template, fill_pairs - ) - except (KeyError, TemplateUnsolved): - keys = fill_pairs.keys() - msg = "Subset grouping failed. " \ - "Only {} are expected in Settings".format(','.join(keys)) - self.log.warning(msg) - - return filled_template - def create_version_data(self, instance): """Create the data collection for the version From d7c5ad1f7c9913a39b43087cebbbee7971844f8c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 14:33:37 +0100 Subject: [PATCH 023/124] Remove duplicate "source" in families --- openpype/plugins/publish/integrate_new.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 284e110916..08088479d0 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -86,7 +86,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "source", "matchmove", "image", - "source", "assembly", "fbx", "textures", From 8fffc60b5016d63d6fad2b8c3b399537a3736171 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 14:37:23 +0100 Subject: [PATCH 024/124] Move remainder of prepare anatomy data to the Collector --- .../plugins/publish/collect_anatomy_context_data.py | 6 ++++++ openpype/plugins/publish/integrate_new.py | 13 ------------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index bd8d9e50c4..346caf6b83 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -91,5 +91,11 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): } }) + intent = context.data.get("intent") + if intent and isinstance(intent, dict): + intent_value = intent.get("value") + if intent_value: + context_data["intent"] = intent_value + self.log.info("Global anatomy Data collected") self.log.debug(json.dumps(context_data, indent=4)) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 08088479d0..f598c540e5 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -119,10 +119,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if families & set(self.exclude_families): return - # TODO: Avoid the need to do any adjustments to anatomy data - # Best case scenario that's all handled by collectors - self.prepare_anatomy(instance) - file_transactions = FileTransaction(log=self.log) try: self.register(instance, file_transactions) @@ -137,15 +133,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # the try, except. file_transactions.finalize() - def prepare_anatomy(self, instance): - """Prepare anatomy data used to define representation destinations""" - # TODO: This logic should move to CollectAnatomyContextData - intent_value = instance.context.data.get("intent") - if intent_value and isinstance(intent_value, dict): - intent_value = intent_value.get("value") - if intent_value: - instance.data["anatomyData"]["intent"] = intent_value - def get_profile_filter_criteria(self, instance): """Return filter criteria for `filter_profiles`""" # Anatomy data is pre-filled by Collectors From 177e83ec8bf55e28ca551affefc4ac775570fe98 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 14:43:00 +0100 Subject: [PATCH 025/124] Restore "published_path" backwards compatibility for IntegrateFtrackInstance on Farm --- openpype/plugins/publish/integrate_new.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index f598c540e5..05cbb357e3 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -532,6 +532,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Store first transferred destination as published path data # todo: can we remove this? published_path = transfers[0][1] + repre["published_path"] = published_path # Backwards compatibility # todo: `repre` is not the actual `representation` entity # we should simplify/clarify difference between data above From 7189954a3c29ca00139a9a50b58606a3c335de04 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 14:44:19 +0100 Subject: [PATCH 026/124] Use `os.path.abspath` instead of `os.path.normpath` when adding a transfer --- openpype/lib/file_transaction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/lib/file_transaction.py b/openpype/lib/file_transaction.py index 57592e297f..1626bec6b6 100644 --- a/openpype/lib/file_transaction.py +++ b/openpype/lib/file_transaction.py @@ -66,8 +66,8 @@ class FileTransaction(object): """Add a new file to transfer queue""" opts = {"mode": mode} - src = os.path.normpath(src) - dst = os.path.normpath(dst) + src = os.path.abspath(src) + dst = os.path.abspath(dst) if dst in self._transfers: queued_src = self._transfers[dst][0] From 8f8b578f0ce660b1c8182ad2486aca21ed1828e2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 19:58:55 +0100 Subject: [PATCH 027/124] Move Subset Grouping Profiles settings to Collect Subset Group - This is moved from the Integrate Asset New settings --- .../plugins/publish/collect_subset_group.py | 16 +-- openpype/plugins/publish/integrate_new.py | 1 - .../defaults/project_settings/global.json | 20 ++-- .../schemas/schema_global_publish.json | 101 ++++++++++-------- 4 files changed, 71 insertions(+), 67 deletions(-) diff --git a/openpype/plugins/publish/collect_subset_group.py b/openpype/plugins/publish/collect_subset_group.py index 60c1c04e70..075699e304 100644 --- a/openpype/plugins/publish/collect_subset_group.py +++ b/openpype/plugins/publish/collect_subset_group.py @@ -24,28 +24,22 @@ class CollectSubsetGroup(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.495 label = "Collect Subset Group" + # Defined in OpenPype settings + subset_grouping_profiles = None + def process(self, instance): """Look into subset group profiles set by settings. Attribute 'subset_grouping_profiles' is defined by OpenPype settings. """ - # TODO: Move this setting to this Collector instead of Integrator - project_settings = instance.context.data["project_settings"] - subset_grouping_profiles = ( - project_settings["global"] - ["publish"] - ["IntegrateAssetNew"] - ["subset_grouping_profiles"] - ) - # Skip if 'subset_grouping_profiles' is empty - if not subset_grouping_profiles: + if not self.subset_grouping_profiles: return # Skip if there is no matching profile filter_criteria = self.get_profile_filter_criteria(instance) - profile = filter_profiles(subset_grouping_profiles, + profile = filter_profiles(self.subset_grouping_profiles, filter_criteria, logger=self.log) if not profile: diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 05cbb357e3..4706d4d093 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -110,7 +110,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Attributes set by settings template_name_profiles = None - subset_grouping_profiles = None def process(self, instance): diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 30a71b044a..528df111f0 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -20,6 +20,17 @@ ], "skip_hosts_headless_publish": [] }, + "CollectSubsetGroup": { + "subset_grouping_profiles": [ + { + "families": [], + "hosts": [], + "task_types": [], + "tasks": [], + "template": "" + } + ] + }, "ValidateEditorialAssetName": { "enabled": true, "optional": false @@ -193,15 +204,6 @@ "tasks": [], "template_name": "render" } - ], - "subset_grouping_profiles": [ - { - "families": [], - "hosts": [], - "task_types": [], - "tasks": [], - "template": "" - } ] }, "CleanUp": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 12043d4205..ab968037f6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -39,6 +39,61 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "CollectSubsetGroup", + "label": "Collect Subset Group", + "is_group": true, + "children": [ + { + "type": "list", + "key": "subset_grouping_profiles", + "label": "Subset grouping profiles", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "label", + "label": "Set all published instances as a part of specific group named according to 'Template'.
Implemented all variants of placeholders [{task},{family},{host},{subset},{renderlayer}]" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "type": "hosts-enum", + "key": "hosts", + "label": "Hosts", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "template", + "label": "Template" + } + ] + } + } + ] + }, { "type": "dict", "collapsible": true, @@ -603,52 +658,6 @@ } ] } - }, - { - "type": "list", - "key": "subset_grouping_profiles", - "label": "Subset grouping profiles", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "label", - "label": "Set all published instances as a part of specific group named according to 'Template'.
Implemented all variants of placeholders [{task},{family},{host},{subset},{renderlayer}]" - }, - { - "key": "families", - "label": "Families", - "type": "list", - "object_type": "text" - }, - { - "type": "hosts-enum", - "key": "hosts", - "label": "Hosts", - "multiselection": true - }, - { - "key": "task_types", - "label": "Task types", - "type": "task-types-enum" - }, - { - "key": "tasks", - "label": "Task names", - "type": "list", - "object_type": "text" - }, - { - "type": "separator" - }, - { - "type": "text", - "key": "template", - "label": "Template" - } - ] - } } ] }, From 6ff7167d54e8a70441300ba4d21acb5a01eb5071 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 20:09:08 +0100 Subject: [PATCH 028/124] Separate get_template_name into its own method + use `self.default_template_name` --- openpype/plugins/publish/integrate_new.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 4706d4d093..c1fa7ccaf2 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -172,14 +172,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) ) - # Define publish template name from profiles - filter_criteria = self.get_profile_filter_criteria(instance) - profile = filter_profiles(self.template_name_profiles, - filter_criteria, - logger=self.log) - template_name = "publish" - if profile: - template_name = profile["template_name"] + template_name = self._get_template_name(instance) subset = self.register_subset(instance) @@ -582,6 +575,19 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return families + def _get_template_name(self, instance): + """Return anatomy template name to use for integration""" + + # Define publish template name from profiles + filter_criteria = self.get_profile_filter_criteria(instance) + profile = filter_profiles(self.template_name_profiles, + filter_criteria, + logger=self.log) + template_name = self.default_template_name + if profile: + template_name = profile["template_name"] + return template_name + def register_subset(self, instance): asset = instance.data.get("assetEntity") subset_name = instance.data["subset"] From 821293d3b855acf2cadd914328a975fd619acd56 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 20:09:31 +0100 Subject: [PATCH 029/124] Match comment from Integrator for consistency --- openpype/plugins/publish/collect_subset_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_subset_group.py b/openpype/plugins/publish/collect_subset_group.py index 075699e304..5756563ed3 100644 --- a/openpype/plugins/publish/collect_subset_group.py +++ b/openpype/plugins/publish/collect_subset_group.py @@ -24,7 +24,7 @@ class CollectSubsetGroup(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.495 label = "Collect Subset Group" - # Defined in OpenPype settings + # Attributes set by settings subset_grouping_profiles = None def process(self, instance): From c3e0162c436a081ccf809cd429f5b828202569d0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 20:11:31 +0100 Subject: [PATCH 030/124] Debug log when exclude family was found for the instance --- openpype/plugins/publish/integrate_new.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index c1fa7ccaf2..8a71c0d5aa 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -115,7 +115,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Exclude instances that also contain families from exclude families families = set(self._get_instance_families(instance)) - if families & set(self.exclude_families): + exclude = families & set(self.exclude_families) + if exclude: + self.log.debug("Instance not integrated due to exclude " + "families found: {}".format(", ".join(exclude))) return file_transactions = FileTransaction(log=self.log) From fbdb385e5b855c0762583256311501b78a2ca730 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 20:20:00 +0100 Subject: [PATCH 031/124] Perform database registering of Subset and Version in a single Bulk Write --- openpype/plugins/publish/integrate_new.py | 30 +++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 8a71c0d5aa..6f1d745b9a 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -177,11 +177,17 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): template_name = self._get_template_name(instance) - subset = self.register_subset(instance) - - version = self.register_version(instance, subset) + subset, subset_writes = self.register_subset(instance) + version, version_writes = self.register_version(instance, subset) instance.data["versionEntity"] = version + # Bulk write to the database + # todo: Try to avoid writing already until after we've prepared + # representations to allow easier rollback? + io._database[io.Session["AVALON_PROJECT"]].bulk_write( + subset_writes + version_writes + ) + archived_repres = list(io.find({ "parent": version["_id"], "type": "archived_representation" @@ -330,16 +336,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): repre["type"] = "archived_representation" bulk_writes.append(InsertOne(repre)) - # bulk updates - # todo: Try to avoid writing already until after we've prepared - # representations to allow easier rollback? - io._database[io.Session["AVALON_PROJECT"]].bulk_write( - bulk_writes - ) - self.log.info("Registered version: v{0:03d}".format(version["name"])) - return version + return version, bulk_writes def prepare_representation(self, repre, template_name, @@ -612,6 +611,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if subset_group: data["subsetGroup"] = subset_group + bulk_writes = [] if subset is None: # Create a new subset self.log.info("Subset '%s' not found, creating ..." % subset_name) @@ -623,22 +623,22 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "data": data, "parent": asset["_id"] } - io.insert_one(subset) + bulk_writes.append(InsertOne(subset)) else: # Update existing subset data with new data and set in database. # We also change the found subset in-place so we don't need to # re-query the subset afterwards subset["data"].update(data) - io.update_many( + bulk_writes.append(UpdateOne( {"type": "subset", "_id": subset["_id"]}, {"$set": { "data": subset["data"] }} - ) + )) self.log.info("Registered subset: {}".format(subset_name)) - return subset + return subset, bulk_writes def create_version_data(self, instance): """Create the data collection for the version From 1844281c68d0e357eccdc8c277db278ef0651f31 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 26 Mar 2022 20:41:22 +0100 Subject: [PATCH 032/124] Match assertion for collection of files (allow no absolute paths) similar to single files --- openpype/plugins/publish/integrate_new.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 6f1d745b9a..ead00452da 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -398,6 +398,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): is_sequence_representation = isinstance(files, (list, tuple)) if is_sequence_representation: # Collection of files (sequence) + assert not any(os.path.isabs(fname) for fname in files), ( + "Given file names contain full paths" + ) + # Get the sequence as a collection. The files must be of a single # sequence and have no remainder outside of the collections. collections, remainder = clique.assemble(files, From 8e0161bec7353bff8bc581d4d676b3ba7c090ba8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 28 Mar 2022 15:04:24 +0200 Subject: [PATCH 033/124] Also Bulk Write representation changes + more cleanup - Don't create intermediate archived representations - Move writing of Subset + Version to database to just before file transactions - Perform ReplaceOne for version instead of update with "$set" for the full version --- openpype/plugins/publish/integrate_new.py | 166 ++++++++++------------ 1 file changed, 79 insertions(+), 87 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index ead00452da..7a3ca2bdf7 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -6,7 +6,7 @@ import clique import six from bson.objectid import ObjectId -from pymongo import DeleteOne, InsertOne, UpdateOne +from pymongo import DeleteMany, ReplaceOne, InsertOne, UpdateOne import pyblish.api from avalon import io import openpype.api @@ -28,6 +28,11 @@ def get_first_frame_padded(collection): return get_frame_padded(start_frame, padding=collection.padding) +def bulk_write(writes): + """Convenience function to bulk write into active project database""" + return io._database[io.Session["AVALON_PROJECT"]].bulk_write(writes) + + class IntegrateAssetNew(pyblish.api.InstancePlugin): """Resolve any dependency issues @@ -177,21 +182,17 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): template_name = self._get_template_name(instance) - subset, subset_writes = self.register_subset(instance) - version, version_writes = self.register_version(instance, subset) + subset, subset_writes = self.prepare_subset(instance) + version, version_writes = self.prepare_version(instance, subset) instance.data["versionEntity"] = version - # Bulk write to the database - # todo: Try to avoid writing already until after we've prepared - # representations to allow easier rollback? - io._database[io.Session["AVALON_PROJECT"]].bulk_write( - subset_writes + version_writes - ) - - archived_repres = list(io.find({ - "parent": version["_id"], - "type": "archived_representation" - })) + # Get existing representations (if any) + existing_repres_by_name = { + repres["name"].lower(): repres for repres in io.find({ + "parent": version["_id"], + "type": "representation" + }) + } # Prepare all representations prepared_representations = [] @@ -205,7 +206,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # todo: reduce/simplify what is returned from this function prepared = self.prepare_representation(repre, template_name, - archived_repres, + existing_repres_by_name, version, instance_stagingdir, instance) @@ -225,40 +226,70 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): for src, dst in instance.data.get("hardlinks", []): file_transactions.add(src, dst, mode=FileTransaction.MODE_HARDLINK) + # Bulk write to the database + # todo: Can we move this even to after the file transfers? + bulk_write(subset_writes + version_writes) + self.log.info("Subset {subset[name]} and Version {version[name]} " + "written to database..".format(subset=subset, + version=version)) + # Process all file transfers of all integrations now self.log.debug("Integrating source files to destination ...") file_transactions.process() - self.log.debug("Backup files " + self.log.debug("Backed up existing files: " "{}".format(file_transactions.backups)) - self.log.debug("Integrated files " + self.log.debug("Transferred files: " "{}".format(file_transactions.transferred)) # Finalize the representations now the published files are integrated # Get 'files' info for representations and its attached resources - self.log.debug("Retrieving Representation files information ...") + self.log.debug("Retrieving Representation Site Sync information ...") sites = SiteSync.compute_resource_sync_sites( system_settings=instance.context.data["system_settings"], project_settings=instance.context.data["project_settings"] ) - log.debug("final sites:: {}".format(sites)) + self.log.debug("final sites:: {}".format(sites)) anatomy = instance.context.data["anatomy"] - representations = [] + representation_writes = [] + new_repre_names_low = set() for prepared in prepared_representations: transfers = prepared["transfers"] representation = prepared["representation"] representation["files"] = self.get_files_info( transfers, sites, anatomy ) - representations.append(representation) - # Remove all archived representations - if archived_repres: - repre_ids_to_remove = [repre["_id"] for repre in archived_repres] - io.delete_many({"_id": {"$in": repre_ids_to_remove}}) + # Set up representation for writing to the database. Since + # we *might* be overwriting an existing entry if the version + # already existed we'll use ReplaceOnce with `upsert=True` + representation_writes.append(ReplaceOne( + filter={"_id": representation["_id"]}, + replacement=representation, + upsert=True + )) - # Write the new representations to the database - io.insert_many(representations) + new_repre_names_low.add(representation["name"].lower()) + + # Delete any existing representations that didn't get any new data + # if the instance is not set to append mode + if not instance.data.get("append", False): + delete_names = set() + for name, existing_repres in existing_repres_by_name.items(): + if name not in new_repre_names_low: + # We add the exact representation name because `name` is + # lowercase for name matching only and not in the database + delete_names.add(existing_repres["name"]) + if delete_names: + representation_writes.append(DeleteMany( + filter={ + "parent": version["_id"], + "name": {"$in": list(delete_names)} + } + )) + + # Write representations to the database + bulk_write(representation_writes) # Backwards compatibility # todo: can we avoid the need to store this? @@ -267,12 +298,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): } self.log.info("Registered {} representations" - "".format(len(representations))) + "".format(len(prepared_representations))) - def register_version(self, instance, subset): + def prepare_version(self, instance, subset): version_number = instance.data["version"] - self.log.debug("Version: v{0:03d}".format(version_number)) version = { "schema": "openpype:version-3.0", @@ -288,61 +318,26 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): 'name': version_number }) - bulk_writes = [] - if existing_version is None: + if existing_version: + self.log.debug("Updating existing version ...") + version["_id"] = existing_version["_id"] + else: self.log.debug("Creating new version ...") version["_id"] = ObjectId() - bulk_writes.append(InsertOne(version)) - else: - self.log.debug("Updating existing version ...") - # Check if instance have set `append` mode which cause that - # only replicated representations are set to archive - append_repres = instance.data.get("append", False) - # Update version data - version_id = existing_version['_id'] - bulk_writes.append(UpdateOne({ - '_id': version_id - }, { - '$set': version - })) + bulk_writes = [ReplaceOne( + filter={"_id": version["_id"]}, + replacement=version, + upsert=True + )] - # Instead of directly writing and querying we reproduce what - # the resulting version would look like so we can hold off making - # changes to the database to avoid the need for 'rollback' - version = copy.deepcopy(version) - version["_id"] = existing_version["_id"] - - # Find representations of existing version and archive them - repres = instance.data.get("representations", []) - new_repre_names_low = [_repre["name"].lower() for _repre in repres] - current_repres = io.find({ - "type": "representation", - "parent": version_id - }) - for repre in current_repres: - if append_repres: - # archive only duplicated representations - if repre["name"].lower() not in new_repre_names_low: - continue - # Representation must change type, - # `_id` must be stored to other key and replaced with new - # - that is because new representations should have same ID - repre_id = repre["_id"] - bulk_writes.append(DeleteOne({"_id": repre_id})) - - repre["orig_id"] = repre_id - repre["_id"] = ObjectId() - repre["type"] = "archived_representation" - bulk_writes.append(InsertOne(repre)) - - self.log.info("Registered version: v{0:03d}".format(version["name"])) + self.log.info("Prepared version: v{0:03d}".format(version["name"])) return version, bulk_writes def prepare_representation(self, repre, template_name, - archived_repres, + existing_repres_by_name, version, instance_stagingdir, instance): @@ -516,15 +511,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if repre.get("udim"): repre_context["udim"] = repre.get("udim") # store list - # Define representation id - repre_id = ObjectId() - # Use previous representation's id if there is a name match - repre_name_lower = repre["name"].lower() - for _archived_repres in archived_repres: - if repre_name_lower == _archived_repres["name"].lower(): - repre_id = _archived_repres["orig_id"] - break + existing = existing_repres_by_name.get(repre["name"].lower()) + if existing: + repre_id = existing["_id"] + else: + repre_id = ObjectId() # Backwards compatibility: # Store first transferred destination as published path data @@ -594,7 +586,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): template_name = profile["template_name"] return template_name - def register_subset(self, instance): + def prepare_subset(self, instance): asset = instance.data.get("assetEntity") subset_name = instance.data["subset"] self.log.debug("Subset: {}".format(subset_name)) @@ -631,7 +623,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): else: # Update existing subset data with new data and set in database. - # We also change the found subset in-place so we don't need to + # We also change the found subset in-place so we don't need to # re-query the subset afterwards subset["data"].update(data) bulk_writes.append(UpdateOne( @@ -641,7 +633,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): }} )) - self.log.info("Registered subset: {}".format(subset_name)) + self.log.info("Prepared subset: {}".format(subset_name)) return subset, bulk_writes def create_version_data(self, instance): From ba2c6e6f084e5829f32250735f13f045cabca800 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 28 Mar 2022 15:43:57 +0200 Subject: [PATCH 034/124] Fix class type --- openpype/plugins/publish/collect_subset_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_subset_group.py b/openpype/plugins/publish/collect_subset_group.py index 5756563ed3..56cd7de94e 100644 --- a/openpype/plugins/publish/collect_subset_group.py +++ b/openpype/plugins/publish/collect_subset_group.py @@ -17,7 +17,7 @@ from openpype.lib import ( ) -class CollectSubsetGroup(pyblish.api.ContextPlugin): +class CollectSubsetGroup(pyblish.api.InstancePlugin): """Collect Subset Group for publish.""" # Run after CollectAnatomyInstanceData From e6665e579ee069b30a02b1034e53d48c85553761 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 28 Mar 2022 20:32:46 +0200 Subject: [PATCH 035/124] Restructure code and more cleanup --- openpype/plugins/publish/integrate_new.py | 250 +++++++++++----------- 1 file changed, 123 insertions(+), 127 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 7a3ca2bdf7..6401806394 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -17,6 +17,21 @@ from openpype.lib.file_transaction import FileTransaction log = logging.getLogger(__name__) +def get_instance_families(instance): + """Get all families of the instance""" + # todo: move this to lib? + family = instance.data.get("family") + families = [] + if family: + families.append(family) + + for _family in (instance.data.get("families") or []): + if _family not in families: + families.append(_family) + + return families + + def get_frame_padded(frame, padding): """Return frame number as string with `padding` amount of padded zeros""" return "{frame:0{padding}d}".format(padding=padding, frame=frame) @@ -119,7 +134,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def process(self, instance): # Exclude instances that also contain families from exclude families - families = set(self._get_instance_families(instance)) + families = set(get_instance_families(instance)) exclude = families & set(self.exclude_families) if exclude: self.log.debug("Instance not integrated due to exclude " @@ -140,22 +155,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # the try, except. file_transactions.finalize() - def get_profile_filter_criteria(self, instance): - """Return filter criteria for `filter_profiles`""" - # Anatomy data is pre-filled by Collectors - anatomy_data = instance.data["anatomyData"] - - # Task can be optional in anatomy data - task = anatomy_data.get("task", {}) - - # Return filter criteria - return { - "families": anatomy_data["family"], - "tasks": task.get("name"), - "hosts": anatomy_data["app"], - "task_types": task.get("type") - } - def register(self, instance, file_transactions): instance_stagingdir = instance.data.get("stagingDir") @@ -171,16 +170,16 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "@ {0}".format(instance_stagingdir) ) - # Ensure at least one file is set up for transfer in staging dir. + # Ensure at least one representation is set up for registering. repres = instance.data.get("representations") - assert repres, "Instance has no files to transfer" + assert repres, "Instance has representations data" assert isinstance(repres, (list, tuple)), ( - "Instance 'files' must be a list, got: {0} {1}".format( + "Instance 'repres' must be a list, got: {0} {1}".format( str(type(repres)), str(repres) ) ) - template_name = self._get_template_name(instance) + template_name = self.get_template_name(instance) subset, subset_writes = self.prepare_subset(instance) version, version_writes = self.prepare_version(instance, subset) @@ -300,6 +299,56 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.info("Registered {} representations" "".format(len(prepared_representations))) + def prepare_subset(self, instance): + asset = instance.data.get("assetEntity") + subset_name = instance.data["subset"] + self.log.debug("Subset: {}".format(subset_name)) + + # Get existing subset if it exists + subset = io.find_one({ + "type": "subset", + "parent": asset["_id"], + "name": subset_name + }) + + # Define subset data + data = { + "families": get_instance_families(instance) + } + + subset_group = instance.data.get("subsetGroup") + if subset_group: + data["subsetGroup"] = subset_group + + bulk_writes = [] + if subset is None: + # Create a new subset + self.log.info("Subset '%s' not found, creating ..." % subset_name) + subset = { + "_id": ObjectId(), + "schema": "openpype:subset-3.0", + "type": "subset", + "name": subset_name, + "data": data, + "parent": asset["_id"] + } + bulk_writes.append(InsertOne(subset)) + + else: + # Update existing subset data with new data and set in database. + # We also change the found subset in-place so we don't need to + # re-query the subset afterwards + subset["data"].update(data) + bulk_writes.append(UpdateOne( + {"type": "subset", "_id": subset["_id"]}, + {"$set": { + "data": subset["data"] + }} + )) + + self.log.info("Prepared subset: {}".format(subset_name)) + return subset, bulk_writes + def prepare_version(self, instance, subset): version_number = instance.data["version"] @@ -559,91 +608,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "published_files": [transfer[1] for transfer in transfers] } - def _get_instance_families(self, instance): - """Get all families of the instance""" - # todo: move this to lib? - family = instance.data.get("family") - families = [] - if family: - families.append(family) - - for _family in (instance.data.get("families") or []): - if _family not in families: - families.append(_family) - - return families - - def _get_template_name(self, instance): - """Return anatomy template name to use for integration""" - - # Define publish template name from profiles - filter_criteria = self.get_profile_filter_criteria(instance) - profile = filter_profiles(self.template_name_profiles, - filter_criteria, - logger=self.log) - template_name = self.default_template_name - if profile: - template_name = profile["template_name"] - return template_name - - def prepare_subset(self, instance): - asset = instance.data.get("assetEntity") - subset_name = instance.data["subset"] - self.log.debug("Subset: {}".format(subset_name)) - - # Get existing subset if it exists - subset = io.find_one({ - "type": "subset", - "parent": asset["_id"], - "name": subset_name - }) - - # Define subset data - data = { - "families": self._get_instance_families(instance) - } - - subset_group = instance.data.get("subsetGroup") - if subset_group: - data["subsetGroup"] = subset_group - - bulk_writes = [] - if subset is None: - # Create a new subset - self.log.info("Subset '%s' not found, creating ..." % subset_name) - subset = { - "_id": ObjectId(), - "schema": "openpype:subset-3.0", - "type": "subset", - "name": subset_name, - "data": data, - "parent": asset["_id"] - } - bulk_writes.append(InsertOne(subset)) - - else: - # Update existing subset data with new data and set in database. - # We also change the found subset in-place so we don't need to - # re-query the subset afterwards - subset["data"].update(data) - bulk_writes.append(UpdateOne( - {"type": "subset", "_id": subset["_id"]}, - {"$set": { - "data": subset["data"] - }} - )) - - self.log.info("Prepared subset: {}".format(subset_name)) - return subset, bulk_writes - def create_version_data(self, instance): - """Create the data collection for the version + """Create the data dictionary for the version Args: instance: the current instance being published Returns: - dict: the required information with instance.data as key + dict: the required information for version["data"] """ context = instance.context @@ -658,7 +630,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.debug("Source: {}".format(source)) version_data = { - "families": self._get_instance_families(instance), + "families": get_instance_families(instance), "time": context.data["time"], "author": context.data["user"], "source": source, @@ -692,28 +664,52 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return version_data - def main_family_from_instance(self, instance): - """Returns main family of entered instance.""" - return self._get_instance_families(instance)[0] + def get_template_name(self, instance): + """Return anatomy template name to use for integration""" + + # Define publish template name from profiles + filter_criteria = self.get_profile_filter_criteria(instance) + profile = filter_profiles(self.template_name_profiles, + filter_criteria, + logger=self.log) + template_name = self.default_template_name + if profile: + template_name = profile["template_name"] + return template_name + + def get_profile_filter_criteria(self, instance): + """Return filter criteria for `filter_profiles`""" + # Anatomy data is pre-filled by Collectors + anatomy_data = instance.data["anatomyData"] + + # Task can be optional in anatomy data + task = anatomy_data.get("task", {}) + + # Return filter criteria + return { + "families": anatomy_data["family"], + "tasks": task.get("name"), + "hosts": anatomy_data["app"], + "task_types": task.get("type") + } def get_rootless_path(self, anatomy, path): - """ Returns, if possible, path without absolute portion from host - (eg. 'c:\' or '/opt/..') - This information is host dependent and shouldn't be captured. - Example: - 'c:/projects/MyProject1/Assets/publish...' > - '{root}/MyProject1/Assets...' + """Returns, if possible, path without absolute portion from root + (eg. 'c:\' or '/opt/..') + + This information is platform dependent and shouldn't be captured. + Example: + 'c:/projects/MyProject1/Assets/publish...' > + '{root}/MyProject1/Assets...' Args: - anatomy: anatomy part from instance - path: path (absolute) + anatomy: anatomy part from instance + path: path (absolute) Returns: - path: modified path if possible, or unmodified path - + warning logged + path: modified path if possible, or unmodified path + + warning logged """ - success, rootless_path = ( - anatomy.find_root_template_from_path(path) - ) + success, rootless_path = anatomy.find_root_template_from_path(path) if success: path = rootless_path else: @@ -731,9 +727,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): Context info. Arguments: - instance: the current instance being published - integrated_file_sizes: dictionary of destination path (absolute) - and its file size + transfers (list): List of transferred files (source, destination) + sites (list): array of published locations + anatomy: anatomy part from instance Returns: output_resources: array of dictionaries to be added to 'files' key in representation @@ -749,14 +745,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): """ Prepare information for one file (asset or resource) Arguments: - path: destination url of published file (rootless) - size(optional): size of file in bytes - file_hash(optional): hash of file for synchronization validation - sites(optional): array of published locations, - [ {'name':'studio', 'created_dt':date} by default - keys expected ['studio', 'site1', 'gdrive1'] + path: destination url of published file + anatomy: anatomy part from instance + sites: array of published locations, + [ {'name':'studio', 'created_dt':date} by default + keys expected ['studio', 'site1', 'gdrive1'] + Returns: - rec: dictionary with filled info + dict: file info dictionary """ file_hash = openpype.api.source_hash(path) From 2777c36eb52e7390b15accc93c9b9a9a771ba21d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 28 Mar 2022 20:34:16 +0200 Subject: [PATCH 036/124] Rely on `instance.data["fps"] over `context.data["fps"]` if available --- openpype/plugins/publish/integrate_new.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 6401806394..00922b0ed3 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -636,9 +636,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "source": source, "comment": context.data.get("comment"), "machine": context.data.get("machine"), - "fps": context.data.get( - "fps", instance.data.get("fps") - ) + "fps": instance.data.get("fps", context.data.get("fps")) } intent_value = context.data.get("intent") From add4958d4c9078b6ecad131f6e40beb66ecdd348 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 09:43:27 +0200 Subject: [PATCH 037/124] Fix message --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 00922b0ed3..f6aa720dbb 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -172,7 +172,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Ensure at least one representation is set up for registering. repres = instance.data.get("representations") - assert repres, "Instance has representations data" + assert repres, "Instance has no representations data" assert isinstance(repres, (list, tuple)), ( "Instance 'repres' must be a list, got: {0} {1}".format( str(type(repres)), str(repres) From 77b5c24370b61615b2380fdc464137d3eba13ab9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 11:44:30 +0200 Subject: [PATCH 038/124] Fix message --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index f6aa720dbb..020b1d2b9c 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -174,7 +174,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): repres = instance.data.get("representations") assert repres, "Instance has no representations data" assert isinstance(repres, (list, tuple)), ( - "Instance 'repres' must be a list, got: {0} {1}".format( + "Instance 'representations' must be a list, got: {0} {1}".format( str(type(repres)), str(repres) ) ) From 127f19873f876d58a2c954c4a56c73ddd4d4d4af Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 11:58:52 +0200 Subject: [PATCH 039/124] Streamlining some code, optimize some database queries with projection --- openpype/plugins/publish/integrate_new.py | 36 ++++++++++++----------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 020b1d2b9c..d869a1b6be 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -187,10 +187,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Get existing representations (if any) existing_repres_by_name = { - repres["name"].lower(): repres for repres in io.find({ - "parent": version["_id"], - "type": "representation" - }) + repres["name"].lower(): repres for repres in io.find( + { + "parent": version["_id"], + "type": "representation" + }, + # Only care about id and name of existing representations + projection={"_id": True, "name": True} + ) } # Prepare all representations @@ -239,16 +243,17 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "{}".format(file_transactions.backups)) self.log.debug("Transferred files: " "{}".format(file_transactions.transferred)) - - # Finalize the representations now the published files are integrated - # Get 'files' info for representations and its attached resources self.log.debug("Retrieving Representation Site Sync information ...") + + # Get the accessible sites for Site Sync sites = SiteSync.compute_resource_sync_sites( system_settings=instance.context.data["system_settings"], project_settings=instance.context.data["project_settings"] ) - self.log.debug("final sites:: {}".format(sites)) + self.log.debug("Site Sync Sites: {}".format(sites)) + # Finalize the representations now the published files are integrated + # Get 'files' info for representations and its attached resources anatomy = instance.context.data["anatomy"] representation_writes = [] new_repre_names_low = set() @@ -365,7 +370,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): 'type': 'version', 'parent': subset["_id"], 'name': version_number - }) + }, projection={"_id": True}) if existing_version: self.log.debug("Updating existing version ...") @@ -576,7 +581,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # todo: `repre` is not the actual `representation` entity # we should simplify/clarify difference between data above # and the actual representation entity for the database - data = repre.get("data") or {} + data = repre.get("data", {}) data.update({'path': published_path, 'template': template}) representation = { "_id": repre_id, @@ -664,16 +669,15 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def get_template_name(self, instance): """Return anatomy template name to use for integration""" - # Define publish template name from profiles filter_criteria = self.get_profile_filter_criteria(instance) profile = filter_profiles(self.template_name_profiles, filter_criteria, logger=self.log) - template_name = self.default_template_name if profile: - template_name = profile["template_name"] - return template_name + return profile["template_name"] + else: + return self.default_template_name def get_profile_filter_criteria(self, instance): """Return filter criteria for `filter_profiles`""" @@ -752,13 +756,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): Returns: dict: file info dictionary """ - file_hash = openpype.api.source_hash(path) - return { "_id": ObjectId(), "path": self.get_rootless_path(anatomy, path), "size": os.path.getsize(path), - "hash": file_hash, + "hash": openpype.api.source_hash(path), "sites": sites } From 0c2c60d37b05411193acf8c60f6a2562463ba558 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 12:23:24 +0200 Subject: [PATCH 040/124] Unify usage of `clique.assemble` --- openpype/plugins/publish/integrate_new.py | 60 ++++++++++++++--------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index d869a1b6be..1ceb99e9fe 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -17,6 +17,41 @@ from openpype.lib.file_transaction import FileTransaction log = logging.getLogger(__name__) +def assemble(files): + """Convenience `clique.assemble` wrapper for files of a single collection. + + Unlike `clique.assemble` this wrapper does not allow more than a single + Collection nor any remainder files. Errors will be raised when not only + a single collection is assembled. + + Returns: + clique.Collection: A single sequence Collection + + Raises: + ValueError: Error is raised when files do not result in a single + collected Collection. + + """ + # todo: move this to lib? + # Get the sequence as a collection. The files must be of a single + # sequence and have no remainder outside of the collections. + patterns = [clique.PATTERNS["frames"]] + collections, remainder = clique.assemble(files, + minimum_items=1, + patterns=patterns) + if not collections: + raise ValueError("No collections found in files: " + "{}".format(files)) + if remainder: + raise ValueError("Files found not detected as part" + " of a sequence: {}".format(remainder)) + if len(collections) > 1: + raise ValueError("Files in sequence are not part of a" + " single sequence collection: " + "{}".format(collections)) + return collections[0] + + def get_instance_families(instance): """Get all families of the instance""" # todo: move this to lib? @@ -451,21 +486,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "Given file names contain full paths" ) - # Get the sequence as a collection. The files must be of a single - # sequence and have no remainder outside of the collections. - collections, remainder = clique.assemble(files, - minimum_items=1) - if not collections: - raise ValueError("No collections found in files: " - "{}".format(files)) - if remainder: - raise ValueError("Files found not detected as part" - " of a sequence: {}".format(remainder)) - if len(collections) > 1: - raise ValueError("Files in sequence are not part of a" - " single sequence collection: " - "{}".format(collections)) - src_collection = collections[0] + src_collection = assemble(files) # If the representation has `frameStart` set it renumbers the # frame indices of the published collection. It will start from @@ -512,14 +533,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): template_filled = anatomy_filled[template_name]["path"] repre_context = template_filled.used_values self.log.debug("Template filled: {}".format(str(template_filled))) - dst_collections, _remainder = clique.assemble( - [os.path.normpath(template_filled)], - minimum_items=1, - patterns=[clique.PATTERNS["frames"]] - ) - assert not _remainder, "This is a bug" - assert len(dst_collections) == 1, "This is a bug" - dst_collection = dst_collections[0] + dst_collection = assemble([os.path.normpath(template_filled)]) # Update the destination indexes and padding dst_collection.indexes.clear() From 44d6199a9e4ea7342fb2ef6bd583e0e373da2545 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 12:28:47 +0200 Subject: [PATCH 041/124] Organize single file code more like sequence file code --- openpype/plugins/publish/integrate_new.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 1ceb99e9fe..1592789390 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -551,21 +551,24 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): else: # Single file - template_data.pop("frame", None) fname = files assert not os.path.isabs(fname), ( "Given file name is a full path" ) - # Store used frame value to template data + + # Manage anatomy template data + template_data.pop("frame", None) if repre.get("udim"): template_data["udim"] = repre["udim"][0] - src = os.path.join(stagingdir, fname) + + # Construct destination filepath from template anatomy_filled = anatomy.format(template_data) template_filled = anatomy_filled[template_name]["path"] repre_context = template_filled.used_values dst = os.path.normpath(template_filled) # Single file transfer + src = os.path.join(stagingdir, fname) transfers = [(src, dst)] for key in self.db_representation_context_keys: From a2a77b8a2099b902e01816ec66a2f308e43004d1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 12:51:08 +0200 Subject: [PATCH 042/124] Cleanup `get_files_info` docstring --- openpype/plugins/publish/integrate_new.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 1592789390..0ee2a6286f 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -739,11 +739,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return path def get_files_info(self, transfers, sites, anatomy): - """ Prepare 'files' portion for attached resources and main asset. - Combining records from 'transfers' and 'hardlinks' parts from - instance. - All attached resources should be added, currently without - Context info. + """Prepare 'files' info portion for representations. Arguments: transfers (list): List of transferred files (source, destination) From 6fe6841c996594871a535daf2c21914e5cc32575 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 13:18:04 +0200 Subject: [PATCH 043/124] Capture edge case where all "representations" are tagged for delete --- openpype/plugins/publish/integrate_new.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 0ee2a6286f..80e1909687 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -255,6 +255,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): prepared_representations.append(prepared) + if not prepared_representations: + # Even though we check `instance.data["representations"]` earlier + # this could still happen if all representations were tagged with + # "delete" and thus are skipped for integration + raise RuntimeError("No representations prepared to publish.") + # Each instance can also have pre-defined transfers not explicitly # part of a representation - like texture resources used by a # .ma representation. Those destination paths are pre-defined, etc. From a7a908d1348381ab0c4df9c29861d7c02be635cb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 13:20:51 +0200 Subject: [PATCH 044/124] Improve docstring --- openpype/plugins/publish/integrate_new.py | 39 +++++++++++------------ 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 80e1909687..8e666f3400 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -84,29 +84,26 @@ def bulk_write(writes): class IntegrateAssetNew(pyblish.api.InstancePlugin): - """Resolve any dependency issues + """Register publish in the database and transfer files to destinations. - This plug-in resolves any paths which, if not updated might break - the published file. + Steps: + 1) Register the subset and version + 2) Transfer the representation files to the destination + 3) Register the representation - The order of families is important, when working with lookdev you want to - first publish the texture, update the texture paths in the nodes and then - publish the shading network. Same goes for file dependent assets. - - Requirements for instance to be correctly integrated - - instance.data['representations'] - must be a list and each member - must be a dictionary with following data: - 'files': list of filenames for sequence, string for single file. - Only the filename is allowed, without the folder path. - 'stagingDir': "path/to/folder/with/files" - 'name': representation name (usually the same as extension) - 'ext': file extension - optional data - "frameStart" - "frameEnd" - 'fps' - "data": additional metadata for each representation. + Requires: + instance.data['representations'] - must be a list and each member + must be a dictionary with following data: + 'files': list of filenames for sequence, string for single file. + Only the filename is allowed, without the folder path. + 'stagingDir': "path/to/folder/with/files" + 'name': representation name (usually the same as extension) + 'ext': file extension + optional data + "frameStart" + "frameEnd" + 'fps' + "data": additional metadata for each representation. """ label = "Integrate Asset New" From 3ec9684239b7afc326cad7e184a7c6ed4e7a6058 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 11:28:13 +0200 Subject: [PATCH 045/124] Only add `frame` to context if used by the destination template --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 3543786949..99a915af73 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -158,7 +158,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): exclude_families = ["clip"] db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", - "family", "hierarchy", "task", "username", "frame" + "family", "hierarchy", "task", "username" ] default_template_name = "publish" From 6733df77f1f693b89078f216457621d129eb4f71 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 11:30:23 +0200 Subject: [PATCH 046/124] Remove double entry of "task" --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 99a915af73..da4dafb133 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -158,7 +158,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): exclude_families = ["clip"] db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", - "family", "hierarchy", "task", "username" + "family", "hierarchy", "username" ] default_template_name = "publish" From c95c9f92b92f37eca20b1dbc82c3ef0620f8f753 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 11:34:52 +0200 Subject: [PATCH 047/124] Add comment --- openpype/plugins/publish/integrate_new.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index da4dafb133..a2943e2972 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -577,6 +577,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): transfers = [(src, dst)] for key in self.db_representation_context_keys: + # Also add these values to the context even if not used by the + # destination template value = template_data.get(key) if not value: continue From 65691bf5207cf57b679dd4b36b3abb6ae57e0be5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 11:36:32 +0200 Subject: [PATCH 048/124] Explain why we write subset+version first --- openpype/plugins/publish/integrate_new.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index a2943e2972..bab46803cb 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -270,7 +270,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): file_transactions.add(src, dst, mode=FileTransaction.MODE_HARDLINK) # Bulk write to the database - # todo: Can we move this even to after the file transfers? + # We write the subset and version to the database before the File + # Transaction to reduce the chances of another publish trying to + # publish to the same version number since that chance can greatly + # increase if the file transaction takes a long time. bulk_write(subset_writes + version_writes) self.log.info("Subset {subset[name]} and Version {version[name]} " "written to database..".format(subset=subset, From 0d83f3c76c880d088de718a416370e69529ad4a5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 11:38:43 +0200 Subject: [PATCH 049/124] Add to do for potential erroneous case --- openpype/plugins/publish/integrate_new.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index bab46803cb..84adccb633 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -602,6 +602,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Backwards compatibility: # Store first transferred destination as published path data # todo: can we remove this? + # todo: We shouldn't change data that makes its way back into + # instance.data[] until we know the publish actually succeeded + # otherwise `published_path` might not actually be valid? published_path = transfers[0][1] repre["published_path"] = published_path # Backwards compatibility From 89376a97e4ef85069a3afdfd5e3115b33bd27284 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 20:47:00 +0200 Subject: [PATCH 050/124] Also include file infos of resource files like textures into each representation - This should fix Site Sync for lookdev textures, etc. --- openpype/plugins/publish/integrate_new.py | 29 ++++++++++++++++------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 84adccb633..25ab7817c9 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -264,10 +264,13 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # part of a representation - like texture resources used by a # .ma representation. Those destination paths are pre-defined, etc. # todo: should we move or simplify this logic? + resource_destinations = set() for src, dst in instance.data.get("transfers", []): file_transactions.add(src, dst, mode=FileTransaction.MODE_COPY) + resource_destinations.add(os.path.abspath(dst)) for src, dst in instance.data.get("hardlinks", []): file_transactions.add(src, dst, mode=FileTransaction.MODE_HARDLINK) + resource_destinations.add(os.path.abspath(dst)) # Bulk write to the database # We write the subset and version to the database before the File @@ -295,18 +298,29 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) self.log.debug("Site Sync Sites: {}".format(sites)) + # Compute the resource file infos once (files belonging to the + # version instance instead of an individual representation) so + # we can re-use those file infos per representation + anatomy = instance.context.data["anatomy"] + resource_file_infos = self.prepare_file_info(resource_destinations, + sites=sites, + anatomy=anatomy) + # Finalize the representations now the published files are integrated # Get 'files' info for representations and its attached resources - anatomy = instance.context.data["anatomy"] representation_writes = [] new_repre_names_low = set() for prepared in prepared_representations: - transfers = prepared["transfers"] representation = prepared["representation"] + transfers = prepared["transfers"] + destinations = [dst for src, dst in transfers] representation["files"] = self.get_files_info( - transfers, sites, anatomy + destinations, sites=sites, anatomy=anatomy ) + # Add the version resource file infos to each representation + representation["files"] += resource_file_infos + # Set up representation for writing to the database. Since # we *might* be overwriting an existing entry if the version # already existed we'll use ReplaceOnce with `upsert=True` @@ -751,11 +765,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ).format(path)) return path - def get_files_info(self, transfers, sites, anatomy): + def get_files_info(self, destinations, sites, anatomy): """Prepare 'files' info portion for representations. Arguments: - transfers (list): List of transferred files (source, destination) + destinations (list): List of transferred file destinations sites (list): array of published locations anatomy: anatomy part from instance Returns: @@ -763,10 +777,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): in representation """ file_infos = [] - for _src, dest in transfers: - file_info = self.prepare_file_info(dest, anatomy, sites=sites) + for file_path in destinations: + file_info = self.prepare_file_info(file_path, anatomy, sites=sites) file_infos.append(file_info) - return file_infos def prepare_file_info(self, path, anatomy, sites): From e6209555b01a0330186bc9176c8331a130325186 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 20:50:13 +0200 Subject: [PATCH 051/124] Match behavior more with what integrator did before refactor --- openpype/plugins/publish/collect_anatomy_context_data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 346caf6b83..c3fabba2ce 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -93,9 +93,9 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): intent = context.data.get("intent") if intent and isinstance(intent, dict): - intent_value = intent.get("value") - if intent_value: - context_data["intent"] = intent_value + intent = intent.get("value") + if intent: + context_data["intent"] = intent self.log.info("Global anatomy Data collected") self.log.debug(json.dumps(context_data, indent=4)) From 52fd21d85494dacd0071a3b08d79dbdd04789b30 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 20:51:56 +0200 Subject: [PATCH 052/124] Add todo/question regarding `intent` --- openpype/plugins/publish/collect_anatomy_context_data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index c3fabba2ce..3f7e65ecd3 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -91,6 +91,8 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): } }) + # todo: some code actually expects the dict itself and others doesn't + # question: what should it be? intent = context.data.get("intent") if intent and isinstance(intent, dict): intent = intent.get("value") From 4c78976d3d834a5cb1fd0bce44f465cbf3ac6375 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 20:55:40 +0200 Subject: [PATCH 053/124] Add todo --- openpype/plugins/publish/integrate_new.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 25ab7817c9..e0c0632548 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -688,6 +688,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "fps": instance.data.get("fps", context.data.get("fps")) } + # todo: preferably we wouldn't need this "if dict" etc. logic and + # instead be able to rely what the input value is if it's set. intent_value = context.data.get("intent") if intent_value and isinstance(intent_value, dict): intent_value = intent_value.get("value") From 3e095bc7554a24ef13282ccfd87e0327eb3b8745 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 20:57:38 +0200 Subject: [PATCH 054/124] Use template name for frame padding anatomy template --- openpype/plugins/publish/integrate_new.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index e0c0632548..0f3b11a025 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -520,8 +520,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if repre.get("frameStart") is not None: index_frame_start = int(repre.get("frameStart")) - # TODO use frame padding from right template group - render_template = anatomy.templates["render"] + render_template = anatomy.templates[template_name] frame_start_padding = int( render_template.get( "frame_padding", From b12b1c80f2facbe343333ba3d70dcbe463383538 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 21:00:10 +0200 Subject: [PATCH 055/124] Never shift udim sequences --- openpype/plugins/publish/integrate_new.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 0f3b11a025..fd0d57c646 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -501,6 +501,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): anatomy = instance.context.data['anatomy'] template = os.path.normpath(anatomy.templates[template_name]["path"]) + is_udim = bool(repre.get("udim")) is_sequence_representation = isinstance(files, (list, tuple)) if is_sequence_representation: # Collection of files (sequence) @@ -517,7 +518,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # frame indices from the source collection. destination_indexes = list(src_collection.indexes) destination_padding = len(get_first_frame_padded(src_collection)) - if repre.get("frameStart") is not None: + if repre.get("frameStart") is not None and not is_udim: index_frame_start = int(repre.get("frameStart")) render_template = anatomy.templates[template_name] @@ -543,7 +544,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # from the source indexes, etc. first_index_padded = get_frame_padded(frame=destination_indexes[0], padding=destination_padding) - if repre.get("udim"): + if is_udim: # UDIM representations handle ranges in a different manner template_data["udim"] = first_index_padded else: @@ -579,7 +580,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Manage anatomy template data template_data.pop("frame", None) - if repre.get("udim"): + if is_udim: template_data["udim"] = repre["udim"][0] # Construct destination filepath from template From f7d35c4fed0885c6656da03eb852706c6bf20117 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 21:01:09 +0200 Subject: [PATCH 056/124] add todo/question --- openpype/plugins/publish/integrate_new.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index fd0d57c646..52c7686473 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -522,6 +522,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): index_frame_start = int(repre.get("frameStart")) render_template = anatomy.templates[template_name] + # todo: should we ALWAYS manage the frame padding even when not + # having `frameStart` set? frame_start_padding = int( render_template.get( "frame_padding", From 70bfdd09b40936efc45efa6bbd1ea029447058f2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 2 Apr 2022 21:07:02 +0200 Subject: [PATCH 057/124] Remove old "dependencies" data --- openpype/plugins/publish/integrate_new.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 52c7686473..37c68ffa6d 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -636,7 +636,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "parent": version["_id"], "name": repre['name'], "data": data, - "dependencies": instance.data.get("dependencies", "").split(), # Imprint shortcut to context for performance reasons. "context": repre_context From 45745cc514236d64cc7f2feddbff9e6217b720fa Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 3 Apr 2022 20:37:28 +0200 Subject: [PATCH 058/124] Improve clarity of comment --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 37c68ffa6d..cb469251e6 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -604,7 +604,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): repre_context[key] = template_data[key] # Explicitly store the full list even though template data might - # have a different value + # have a different value because it uses just a single udim tile if repre.get("udim"): repre_context["udim"] = repre.get("udim") # store list From fe72197a9feb413c8f6c5f9e02339ed891fdda07 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 3 Apr 2022 20:40:25 +0200 Subject: [PATCH 059/124] Add comment --- openpype/plugins/publish/integrate_new.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index cb469251e6..f1cceb9ca7 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -156,11 +156,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "usdOverride" ] exclude_families = ["clip"] + default_template_name = "publish" + + # Representation context keys that should always be written to + # the database even if not used by the destination template db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", "family", "hierarchy", "username" ] - default_template_name = "publish" # Attributes set by settings template_name_profiles = None From c3c8281e0134222677b32f91ec644322dd996a74 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 3 Apr 2022 20:41:34 +0200 Subject: [PATCH 060/124] tweak comment --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index f1cceb9ca7..238ae82bba 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -183,7 +183,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.register(instance, file_transactions) except Exception: # clean destination - # todo: rollback any registered entities? (or how safe are we?) + # todo: preferably we'd also rollback *any* changes to the database file_transactions.rollback() self.log.critical("Error when registering", exc_info=True) six.reraise(*sys.exc_info()) From 2e2deb349d082096d056f878a5cf629c2f95e12c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 14 Apr 2022 13:06:14 +0200 Subject: [PATCH 061/124] Match changes that were made to original IntegrateAsset Changes of: - https://github.com/pypeclub/OpenPype/commit/312d0309ab92de834629c58587f1a758d1d1e90c - https://github.com/pypeclub/OpenPype/commit/507f3615ab8f42f5664afcac01d339e0517afdf5 - https://github.com/pypeclub/OpenPype/commit/29dca65202d45a79e66c619b95d3408e227a9c05 --- openpype/plugins/publish/integrate_new.py | 61 ++++++++++++++++++----- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 9e8dfefc9e..768c413bf9 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -4,6 +4,7 @@ import sys import copy import clique import six +from collections import deque, defaultdict from bson.objectid import ObjectId from pymongo import DeleteMany, ReplaceOne, InsertOne, UpdateOne @@ -871,18 +872,18 @@ class SiteSync(object): attached_sites[remote_site] = create_metadata(remote_site, created=False) + # add alternative sites + cls._add_alternative_sites(system_sync_server_presets, attached_sites) + # add skeleton for sites where it should be always synced to always_accessible_sites = ( sync_project_presets["config"].get("always_accessible_on", []) ) - for site in always_accessible_sites: + for site in set(always_accessible_sites): site = site.strip() if site not in attached_sites: attached_sites[site] = create_metadata(site, created=False) - # add alternative sites - cls._add_alternative_sites(system_sync_server_presets, attached_sites) - return list(attached_sites.values()) @staticmethod @@ -904,8 +905,9 @@ class SiteSync(object): return local_site, remote_site - @staticmethod - def _add_alternative_sites(system_sync_server_presets, + @classmethod + def _add_alternative_sites(cls, + system_sync_server_presets, attached_sites): """Loop through all configured sites and add alternatives. @@ -916,18 +918,14 @@ class SiteSync(object): See SyncServerModule.handle_alternate_site """ conf_sites = system_sync_server_presets.get("sites", {}) + alt_site_pairs = cls._get_alt_site_pairs(conf_sites) - for site_name, site_info in conf_sites.items(): + for site_name, alt_sites in alt_site_pairs.items(): # Skip if already defined if site_name in attached_sites: continue - # Get alternate sites (stripped names) for this site name - alt_sites = site_info.get("alternative_sites", []) - alt_sites = [site.strip() for site in alt_sites] - alt_sites = set(alt_sites) - # If no alternative sites we don't need to add if not alt_sites: continue @@ -944,3 +942,42 @@ class SiteSync(object): # Note: We change mutable `attached_site` dict in-place attached_sites[site_name] = alt_site_meta + + @staticmethod + def _get_alt_site_pairs(conf_sites): + """Returns dict of site and its alternative sites. + If `site` has alternative site, it means that alt_site has + 'site' as + alternative site + Args: + conf_sites (dict) + Returns: + (dict): {'site': [alternative sites]...} + """ + alt_site_pairs = defaultdict(list) + for site_name, site_info in conf_sites.items(): + alt_sites = set(site_info.get("alternative_sites", [])) + alt_site_pairs[site_name].extend(alt_sites) + + for alt_site in alt_sites: + alt_site_pairs[alt_site].append(site_name) + + for site_name, alt_sites in alt_site_pairs.items(): + sites_queue = deque(alt_sites) + while sites_queue: + alt_site = sites_queue.popleft() + + # safety against wrong config + # {"SFTP": {"alternative_site": "SFTP"} + if alt_site == site_name or alt_site not in alt_site_pairs: + continue + + for alt_alt_site in alt_site_pairs[alt_site]: + if ( + alt_alt_site != site_name + and alt_alt_site not in alt_sites + ): + alt_sites.append(alt_alt_site) + sites_queue.append(alt_alt_site) + + return alt_site_pairs From 0fdd4f1aecd3b5fa09496d4aa48ee605a003e61d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 14 Apr 2022 13:07:33 +0200 Subject: [PATCH 062/124] Fix indentation --- openpype/plugins/publish/integrate_new.py | 66 +++++++++++------------ 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 768c413bf9..4eccce4e81 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -943,41 +943,41 @@ class SiteSync(object): # Note: We change mutable `attached_site` dict in-place attached_sites[site_name] = alt_site_meta - @staticmethod - def _get_alt_site_pairs(conf_sites): - """Returns dict of site and its alternative sites. - If `site` has alternative site, it means that alt_site has - 'site' as - alternative site - Args: - conf_sites (dict) - Returns: - (dict): {'site': [alternative sites]...} - """ - alt_site_pairs = defaultdict(list) - for site_name, site_info in conf_sites.items(): - alt_sites = set(site_info.get("alternative_sites", [])) - alt_site_pairs[site_name].extend(alt_sites) + @staticmethod + def _get_alt_site_pairs(conf_sites): + """Returns dict of site and its alternative sites. + If `site` has alternative site, it means that alt_site has + 'site' as + alternative site + Args: + conf_sites (dict) + Returns: + (dict): {'site': [alternative sites]...} + """ + alt_site_pairs = defaultdict(list) + for site_name, site_info in conf_sites.items(): + alt_sites = set(site_info.get("alternative_sites", [])) + alt_site_pairs[site_name].extend(alt_sites) - for alt_site in alt_sites: - alt_site_pairs[alt_site].append(site_name) + for alt_site in alt_sites: + alt_site_pairs[alt_site].append(site_name) - for site_name, alt_sites in alt_site_pairs.items(): - sites_queue = deque(alt_sites) - while sites_queue: - alt_site = sites_queue.popleft() + for site_name, alt_sites in alt_site_pairs.items(): + sites_queue = deque(alt_sites) + while sites_queue: + alt_site = sites_queue.popleft() - # safety against wrong config - # {"SFTP": {"alternative_site": "SFTP"} - if alt_site == site_name or alt_site not in alt_site_pairs: - continue + # safety against wrong config + # {"SFTP": {"alternative_site": "SFTP"} + if alt_site == site_name or alt_site not in alt_site_pairs: + continue - for alt_alt_site in alt_site_pairs[alt_site]: - if ( - alt_alt_site != site_name - and alt_alt_site not in alt_sites - ): - alt_sites.append(alt_alt_site) - sites_queue.append(alt_alt_site) + for alt_alt_site in alt_site_pairs[alt_site]: + if ( + alt_alt_site != site_name + and alt_alt_site not in alt_sites + ): + alt_sites.append(alt_alt_site) + sites_queue.append(alt_alt_site) - return alt_site_pairs + return alt_site_pairs From 1a03bbe48a37cc62918152985488a0bd99d43473 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 14 Apr 2022 13:11:57 +0200 Subject: [PATCH 063/124] Store alt sites in a `set` --- openpype/plugins/publish/integrate_new.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 4eccce4e81..2795b59482 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -954,13 +954,13 @@ class SiteSync(object): Returns: (dict): {'site': [alternative sites]...} """ - alt_site_pairs = defaultdict(list) + alt_site_pairs = defaultdict(set) for site_name, site_info in conf_sites.items(): alt_sites = set(site_info.get("alternative_sites", [])) - alt_site_pairs[site_name].extend(alt_sites) + alt_site_pairs[site_name].update(alt_sites) for alt_site in alt_sites: - alt_site_pairs[alt_site].append(site_name) + alt_site_pairs[alt_site].add(site_name) for site_name, alt_sites in alt_site_pairs.items(): sites_queue = deque(alt_sites) @@ -977,7 +977,7 @@ class SiteSync(object): alt_alt_site != site_name and alt_alt_site not in alt_sites ): - alt_sites.append(alt_alt_site) + alt_sites.add(alt_alt_site) sites_queue.append(alt_alt_site) return alt_site_pairs From 8a970b123c1697d7db28f31debf4f7113c3c3177 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 21 Apr 2022 11:24:49 +0200 Subject: [PATCH 064/124] Use logic directly from Sync Server module --- openpype/plugins/publish/integrate_new.py | 165 +--------------------- 1 file changed, 6 insertions(+), 159 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 2795b59482..cc6856e407 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -4,14 +4,13 @@ import sys import copy import clique import six -from collections import deque, defaultdict from bson.objectid import ObjectId from pymongo import DeleteMany, ReplaceOne, InsertOne, UpdateOne import pyblish.api from avalon import io import openpype.api -from datetime import datetime +from openpype.modules import ModulesManager from openpype.lib.profiles_filtering import filter_profiles from openpype.lib.file_transaction import FileTransaction @@ -299,11 +298,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.debug("Retrieving Representation Site Sync information ...") # Get the accessible sites for Site Sync - sites = SiteSync.compute_resource_sync_sites( - system_settings=instance.context.data["system_settings"], - project_settings=instance.context.data["project_settings"] + manager = ModulesManager() + sync_server_module = manager.modules_by_name["sync_server"] + sites = sync_server_module.compute_resource_sync_sites( + project_name=instance.data["projectEntity"]["name"] ) - self.log.debug("Site Sync Sites: {}".format(sites)) + self.log.debug("Sync Server Sites: {}".format(sites)) # Compute the resource file infos once (files belonging to the # version instance instead of an individual representation) so @@ -828,156 +828,3 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "hash": openpype.api.source_hash(path), "sites": sites } - - -class SiteSync(object): - """Logic for Site Sync Module functionality""" - - @classmethod - def compute_resource_sync_sites(cls, - system_settings, - project_settings): - """Get available resource sync sites""" - - def create_metadata(name, created=True): - """Create sync site metadata for site with `name`""" - metadata = {"name": name} - if created: - metadata["created_dt"] = datetime.now() - return metadata - - default_sites = [create_metadata("studio")] - - # If sync site module is disabled return default fallback site - system_sync_server_presets = system_settings["modules"]["sync_server"] - log.debug("system_sett:: {}".format(system_sync_server_presets)) - if not system_sync_server_presets["enabled"]: - return default_sites - - # If sync site module is disabled in current - # project return default fallback site - sync_project_presets = project_settings["global"]["sync_server"] - if not sync_project_presets["enabled"]: - return default_sites - - local_site, remote_site = cls._get_sites(sync_project_presets) - - # Attached sites metadata by site name - # That is the local site, remote site, the always accesible sites - # and their alternate sites (alias of sites with different protocol) - attached_sites = dict() - attached_sites[local_site] = create_metadata(local_site) - - if remote_site and remote_site != local_site: - attached_sites[remote_site] = create_metadata(remote_site, - created=False) - - # add alternative sites - cls._add_alternative_sites(system_sync_server_presets, attached_sites) - - # add skeleton for sites where it should be always synced to - always_accessible_sites = ( - sync_project_presets["config"].get("always_accessible_on", []) - ) - for site in set(always_accessible_sites): - site = site.strip() - if site not in attached_sites: - attached_sites[site] = create_metadata(site, created=False) - - return list(attached_sites.values()) - - @staticmethod - def _get_sites(sync_project_presets): - """Returns tuple (local_site, remote_site)""" - local_site_id = openpype.api.get_local_site_id() - local_site = sync_project_presets["config"]. \ - get("active_site", "studio").strip() - - if local_site == 'local': - local_site = local_site_id - - remote_site = sync_project_presets["config"].get("remote_site") - if remote_site: - remote_site.strip() - - if remote_site == 'local': - remote_site = local_site_id - - return local_site, remote_site - - @classmethod - def _add_alternative_sites(cls, - system_sync_server_presets, - attached_sites): - """Loop through all configured sites and add alternatives. - - For all sites if an alternative site is detected that has an - accessible site then we can also register to that alternative site - with the same "created" state. So we match the existing data. - - See SyncServerModule.handle_alternate_site - """ - conf_sites = system_sync_server_presets.get("sites", {}) - alt_site_pairs = cls._get_alt_site_pairs(conf_sites) - - for site_name, alt_sites in alt_site_pairs.items(): - - # Skip if already defined - if site_name in attached_sites: - continue - - # If no alternative sites we don't need to add - if not alt_sites: - continue - - # Take a copy of data of the first alternate site that is already - # defined as an attached site to match the same state. - match_meta = next((attached_sites[site] for site in alt_sites - if site in attached_sites), None) - if not match_meta: - continue - - alt_site_meta = copy.deepcopy(match_meta) - alt_site_meta["name"] = site_name - - # Note: We change mutable `attached_site` dict in-place - attached_sites[site_name] = alt_site_meta - - @staticmethod - def _get_alt_site_pairs(conf_sites): - """Returns dict of site and its alternative sites. - If `site` has alternative site, it means that alt_site has - 'site' as - alternative site - Args: - conf_sites (dict) - Returns: - (dict): {'site': [alternative sites]...} - """ - alt_site_pairs = defaultdict(set) - for site_name, site_info in conf_sites.items(): - alt_sites = set(site_info.get("alternative_sites", [])) - alt_site_pairs[site_name].update(alt_sites) - - for alt_site in alt_sites: - alt_site_pairs[alt_site].add(site_name) - - for site_name, alt_sites in alt_site_pairs.items(): - sites_queue = deque(alt_sites) - while sites_queue: - alt_site = sites_queue.popleft() - - # safety against wrong config - # {"SFTP": {"alternative_site": "SFTP"} - if alt_site == site_name or alt_site not in alt_site_pairs: - continue - - for alt_alt_site in alt_site_pairs[alt_site]: - if ( - alt_alt_site != site_name - and alt_alt_site not in alt_sites - ): - alt_sites.add(alt_alt_site) - sites_queue.append(alt_alt_site) - - return alt_site_pairs From ae1acb950bbb69b203c36f19d40e3952eca46bfd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 21 Apr 2022 14:08:53 +0200 Subject: [PATCH 065/124] Fix: refactor to use correct function --- openpype/plugins/publish/integrate_new.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index cc6856e407..419e2b4e4b 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -309,9 +309,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # version instance instead of an individual representation) so # we can re-use those file infos per representation anatomy = instance.context.data["anatomy"] - resource_file_infos = self.prepare_file_info(resource_destinations, - sites=sites, - anatomy=anatomy) + resource_file_infos = self.get_files_info(resource_destinations, + sites=sites, + anatomy=anatomy) # Finalize the representations now the published files are integrated # Get 'files' info for representations and its attached resources From fe77fe64adfcd2bf2c9eb77d6c5181c0fb20dd5f Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 22 Jun 2022 17:10:36 +0200 Subject: [PATCH 066/124] add new studios to main page --- website/src/pages/index.js | 27 ++++++++++++++++++- website/static/img/Logo_On_White-HR.png | Bin 0 -> 77588 bytes website/static/img/NoGhost_Logo_black.svg | 31 ++++++++++++++++++++++ website/static/img/agora_studio.png | Bin 0 -> 133985 bytes website/static/img/igg-logo.png | Bin 80331 -> 96336 bytes website/static/img/methodmadness.png | Bin 0 -> 8650 bytes website/static/img/noghost.png | Bin 0 -> 22435 bytes website/static/img/staticvfx.png | Bin 0 -> 12912 bytes 8 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 website/static/img/Logo_On_White-HR.png create mode 100644 website/static/img/NoGhost_Logo_black.svg create mode 100644 website/static/img/agora_studio.png create mode 100644 website/static/img/methodmadness.png create mode 100644 website/static/img/noghost.png create mode 100644 website/static/img/staticvfx.png diff --git a/website/src/pages/index.js b/website/src/pages/index.js index 0886706015..ae7119e928 100644 --- a/website/src/pages/index.js +++ b/website/src/pages/index.js @@ -153,7 +153,32 @@ const studios = [ title: "IGG Canada", image: "/img/igg-logo.png", infoLink: "https://www.igg.com/", - } + }, + { + title: "Agora Studio", + image: "/img/agora_studio.png", + infoLink: "https://agora.studio/", + }, + { + title: "Lucan Visuals", + image: "/img/lucan_Logo_On_White-HR.png", + infoLink: "https://www.lucan.tv/", + }, + { + title: "No Ghost", + image: "/img/noghost.png", + infoLink: "https://www.noghost.co.uk/", + }, + { + title: "Static VFX", + image: "/img/staticvfx.png", + infoLink: "http://www.staticvfx.com/", + }, + { + title: "Method n Madness", + image: "/img/methodmadness.png", + infoLink: "https://www.methodnmadness.com/", +} ]; function Service({imageUrl, title, description}) { diff --git a/website/static/img/Logo_On_White-HR.png b/website/static/img/Logo_On_White-HR.png new file mode 100644 index 0000000000000000000000000000000000000000..c86030e1e78092c7ecd1de2cd129198c99680ee4 GIT binary patch literal 77588 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 + + + + + + + + + + + + diff --git a/website/static/img/agora_studio.png b/website/static/img/agora_studio.png new file mode 100644 index 0000000000000000000000000000000000000000..48b07b877504267eb302aa7ebf9e0b49540ed686 GIT binary patch literal 133985 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�Hb3N@C8UbRsXio1UqFP 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%0A%VsM|U#I>^O_ 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 D(4BF@ literal 0 HcmV?d00001 diff --git a/website/static/img/staticvfx.png b/website/static/img/staticvfx.png new file mode 100644 index 0000000000000000000000000000000000000000..41efd7f12005db1c67e168d1447eaa0596603e1b GIT binary patch literal 12912 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 Date: Wed, 22 Jun 2022 17:11:34 +0200 Subject: [PATCH 067/124] fix lucan logo name --- ...o_On_White-HR.png => lucan_Logo_On_White-HR.png} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename website/static/img/{Logo_On_White-HR.png => lucan_Logo_On_White-HR.png} (100%) diff --git a/website/static/img/Logo_On_White-HR.png b/website/static/img/lucan_Logo_On_White-HR.png similarity index 100% rename from website/static/img/Logo_On_White-HR.png rename to website/static/img/lucan_Logo_On_White-HR.png From 3e058c6e8ac79ebb9933d0ad02957b0467f3a578 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Jul 2022 09:20:00 +0200 Subject: [PATCH 068/124] Move IntegrateAsset --- openpype/plugins/publish/integrate.py | 832 ++++++++++++++++++++++++++ 1 file changed, 832 insertions(+) create mode 100644 openpype/plugins/publish/integrate.py diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py new file mode 100644 index 0000000000..6ad0849ff7 --- /dev/null +++ b/openpype/plugins/publish/integrate.py @@ -0,0 +1,832 @@ +import os +import logging +import sys +import copy +import clique +import six + +from bson.objectid import ObjectId +from pymongo import DeleteMany, ReplaceOne, InsertOne, UpdateOne +import pyblish.api + +import openpype.api +from openpype.modules import ModulesManager +from openpype.lib.profiles_filtering import filter_profiles +from openpype.lib.file_transaction import FileTransaction +from openpype.pipeline import legacy_io + +log = logging.getLogger(__name__) + + +def assemble(files): + """Convenience `clique.assemble` wrapper for files of a single collection. + + Unlike `clique.assemble` this wrapper does not allow more than a single + Collection nor any remainder files. Errors will be raised when not only + a single collection is assembled. + + Returns: + clique.Collection: A single sequence Collection + + Raises: + ValueError: Error is raised when files do not result in a single + collected Collection. + + """ + # todo: move this to lib? + # Get the sequence as a collection. The files must be of a single + # sequence and have no remainder outside of the collections. + patterns = [clique.PATTERNS["frames"]] + collections, remainder = clique.assemble(files, + minimum_items=1, + patterns=patterns) + if not collections: + raise ValueError("No collections found in files: " + "{}".format(files)) + if remainder: + raise ValueError("Files found not detected as part" + " of a sequence: {}".format(remainder)) + if len(collections) > 1: + raise ValueError("Files in sequence are not part of a" + " single sequence collection: " + "{}".format(collections)) + return collections[0] + + +def get_instance_families(instance): + """Get all families of the instance""" + # todo: move this to lib? + family = instance.data.get("family") + families = [] + if family: + families.append(family) + + for _family in (instance.data.get("families") or []): + if _family not in families: + families.append(_family) + + return families + + +def get_frame_padded(frame, padding): + """Return frame number as string with `padding` amount of padded zeros""" + return "{frame:0{padding}d}".format(padding=padding, frame=frame) + + +def get_first_frame_padded(collection): + """Return first frame as padded number from `clique.Collection`""" + start_frame = next(iter(collection.indexes)) + return get_frame_padded(start_frame, padding=collection.padding) + + +def bulk_write(writes): + """Convenience function to bulk write into active project database""" + project = legacy_io.Session["AVALON_PROJECT"] + return legacy_io._database[project].bulk_write(writes) + + +class IntegrateAsset(pyblish.api.InstancePlugin): + """Register publish in the database and transfer files to destinations. + + Steps: + 1) Register the subset and version + 2) Transfer the representation files to the destination + 3) Register the representation + + Requires: + instance.data['representations'] - must be a list and each member + must be a dictionary with following data: + 'files': list of filenames for sequence, string for single file. + Only the filename is allowed, without the folder path. + 'stagingDir': "path/to/folder/with/files" + 'name': representation name (usually the same as extension) + 'ext': file extension + optional data + "frameStart" + "frameEnd" + 'fps' + "data": additional metadata for each representation. + """ + + label = "Integrate Asset New" + order = pyblish.api.IntegratorOrder + families = ["workfile", + "pointcache", + "camera", + "animation", + "model", + "mayaAscii", + "mayaScene", + "setdress", + "layout", + "ass", + "vdbcache", + "scene", + "vrayproxy", + "vrayscene_layer", + "render", + "prerender", + "imagesequence", + "review", + "rendersetup", + "rig", + "plate", + "look", + "audio", + "yetiRig", + "yeticache", + "nukenodes", + "gizmo", + "source", + "matchmove", + "image", + "assembly", + "fbx", + "textures", + "action", + "harmony.template", + "harmony.palette", + "editorial", + "background", + "camerarig", + "redshiftproxy", + "effect", + "xgen", + "hda", + "usd", + "staticMesh", + "skeletalMesh", + "usdComposition", + "usdOverride", + "simpleUnrealTexture" + ] + exclude_families = ["clip", "render.farm"] + default_template_name = "publish" + + # Representation context keys that should always be written to + # the database even if not used by the destination template + db_representation_context_keys = [ + "project", "asset", "task", "subset", "version", "representation", + "family", "hierarchy", "username" + ] + + # Attributes set by settings + template_name_profiles = None + + def process(self, instance): + + # Exclude instances that also contain families from exclude families + families = set(get_instance_families(instance)) + exclude = families & set(self.exclude_families) + if exclude: + self.log.debug("Instance not integrated due to exclude " + "families found: {}".format(", ".join(exclude))) + return + + file_transactions = FileTransaction(log=self.log) + try: + self.register(instance, file_transactions) + except Exception: + # clean destination + # todo: preferably we'd also rollback *any* changes to the database + file_transactions.rollback() + self.log.critical("Error when registering", exc_info=True) + six.reraise(*sys.exc_info()) + + # Finalizing can't rollback safely so no use for moving it to + # the try, except. + file_transactions.finalize() + + def register(self, instance, file_transactions): + + instance_stagingdir = instance.data.get("stagingDir") + if not instance_stagingdir: + self.log.info(( + "{0} is missing reference to staging directory." + " Will try to get it from representation." + ).format(instance)) + + else: + self.log.debug( + "Establishing staging directory " + "@ {0}".format(instance_stagingdir) + ) + + # Ensure at least one representation is set up for registering. + repres = instance.data.get("representations") + assert repres, "Instance has no representations data" + assert isinstance(repres, (list, tuple)), ( + "Instance 'representations' must be a list, got: {0} {1}".format( + str(type(repres)), str(repres) + ) + ) + + template_name = self.get_template_name(instance) + + subset, subset_writes = self.prepare_subset(instance) + version, version_writes = self.prepare_version(instance, subset) + instance.data["versionEntity"] = version + + # Get existing representations (if any) + existing_repres_by_name = { + repres["name"].lower(): repres for repres in legacy_io.find( + { + "parent": version["_id"], + "type": "representation" + }, + # Only care about id and name of existing representations + projection={"_id": True, "name": True} + ) + } + + # Prepare all representations + prepared_representations = [] + for repre in instance.data["representations"]: + + if "delete" in repre.get("tags", []): + self.log.debug("Skipping representation marked for deletion: " + "{}".format(repre)) + continue + + # todo: reduce/simplify what is returned from this function + prepared = self.prepare_representation(repre, + template_name, + existing_repres_by_name, + version, + instance_stagingdir, + instance) + + for src, dst in prepared["transfers"]: + # todo: add support for hardlink transfers + file_transactions.add(src, dst) + + prepared_representations.append(prepared) + + if not prepared_representations: + # Even though we check `instance.data["representations"]` earlier + # this could still happen if all representations were tagged with + # "delete" and thus are skipped for integration + raise RuntimeError("No representations prepared to publish.") + + # Each instance can also have pre-defined transfers not explicitly + # part of a representation - like texture resources used by a + # .ma representation. Those destination paths are pre-defined, etc. + # todo: should we move or simplify this logic? + resource_destinations = set() + for src, dst in instance.data.get("transfers", []): + file_transactions.add(src, dst, mode=FileTransaction.MODE_COPY) + resource_destinations.add(os.path.abspath(dst)) + for src, dst in instance.data.get("hardlinks", []): + file_transactions.add(src, dst, mode=FileTransaction.MODE_HARDLINK) + resource_destinations.add(os.path.abspath(dst)) + + # Bulk write to the database + # We write the subset and version to the database before the File + # Transaction to reduce the chances of another publish trying to + # publish to the same version number since that chance can greatly + # increase if the file transaction takes a long time. + bulk_write(subset_writes + version_writes) + self.log.info("Subset {subset[name]} and Version {version[name]} " + "written to database..".format(subset=subset, + version=version)) + + # Process all file transfers of all integrations now + self.log.debug("Integrating source files to destination ...") + file_transactions.process() + self.log.debug("Backed up existing files: " + "{}".format(file_transactions.backups)) + self.log.debug("Transferred files: " + "{}".format(file_transactions.transferred)) + self.log.debug("Retrieving Representation Site Sync information ...") + + # Get the accessible sites for Site Sync + manager = ModulesManager() + sync_server_module = manager.modules_by_name["sync_server"] + sites = sync_server_module.compute_resource_sync_sites( + project_name=instance.data["projectEntity"]["name"] + ) + self.log.debug("Sync Server Sites: {}".format(sites)) + + # Compute the resource file infos once (files belonging to the + # version instance instead of an individual representation) so + # we can re-use those file infos per representation + anatomy = instance.context.data["anatomy"] + resource_file_infos = self.get_files_info(resource_destinations, + sites=sites, + anatomy=anatomy) + + # Finalize the representations now the published files are integrated + # Get 'files' info for representations and its attached resources + representation_writes = [] + new_repre_names_low = set() + for prepared in prepared_representations: + representation = prepared["representation"] + transfers = prepared["transfers"] + destinations = [dst for src, dst in transfers] + representation["files"] = self.get_files_info( + destinations, sites=sites, anatomy=anatomy + ) + + # Add the version resource file infos to each representation + representation["files"] += resource_file_infos + + # Set up representation for writing to the database. Since + # we *might* be overwriting an existing entry if the version + # already existed we'll use ReplaceOnce with `upsert=True` + representation_writes.append(ReplaceOne( + filter={"_id": representation["_id"]}, + replacement=representation, + upsert=True + )) + + new_repre_names_low.add(representation["name"].lower()) + + # Delete any existing representations that didn't get any new data + # if the instance is not set to append mode + if not instance.data.get("append", False): + delete_names = set() + for name, existing_repres in existing_repres_by_name.items(): + if name not in new_repre_names_low: + # We add the exact representation name because `name` is + # lowercase for name matching only and not in the database + delete_names.add(existing_repres["name"]) + if delete_names: + representation_writes.append(DeleteMany( + filter={ + "parent": version["_id"], + "name": {"$in": list(delete_names)} + } + )) + + # Write representations to the database + bulk_write(representation_writes) + + # Backwards compatibility + # todo: can we avoid the need to store this? + instance.data["published_representations"] = { + p["representation"]["_id"]: p for p in prepared_representations + } + + self.log.info("Registered {} representations" + "".format(len(prepared_representations))) + + def prepare_subset(self, instance): + asset = instance.data.get("assetEntity") + subset_name = instance.data["subset"] + self.log.debug("Subset: {}".format(subset_name)) + + # Get existing subset if it exists + subset = legacy_io.find_one({ + "type": "subset", + "parent": asset["_id"], + "name": subset_name + }) + + # Define subset data + data = { + "families": get_instance_families(instance) + } + + subset_group = instance.data.get("subsetGroup") + if subset_group: + data["subsetGroup"] = subset_group + + bulk_writes = [] + if subset is None: + # Create a new subset + self.log.info("Subset '%s' not found, creating ..." % subset_name) + subset = { + "_id": ObjectId(), + "schema": "openpype:subset-3.0", + "type": "subset", + "name": subset_name, + "data": data, + "parent": asset["_id"] + } + bulk_writes.append(InsertOne(subset)) + + else: + # Update existing subset data with new data and set in database. + # We also change the found subset in-place so we don't need to + # re-query the subset afterwards + subset["data"].update(data) + bulk_writes.append(UpdateOne( + {"type": "subset", "_id": subset["_id"]}, + {"$set": { + "data": subset["data"] + }} + )) + + self.log.info("Prepared subset: {}".format(subset_name)) + return subset, bulk_writes + + def prepare_version(self, instance, subset): + + version_number = instance.data["version"] + + version = { + "schema": "openpype:version-3.0", + "type": "version", + "parent": subset["_id"], + "name": version_number, + "data": self.create_version_data(instance) + } + + existing_version = legacy_io.find_one({ + 'type': 'version', + 'parent': subset["_id"], + 'name': version_number + }, projection={"_id": True}) + + if existing_version: + self.log.debug("Updating existing version ...") + version["_id"] = existing_version["_id"] + else: + self.log.debug("Creating new version ...") + version["_id"] = ObjectId() + + bulk_writes = [ReplaceOne( + filter={"_id": version["_id"]}, + replacement=version, + upsert=True + )] + + self.log.info("Prepared version: v{0:03d}".format(version["name"])) + + return version, bulk_writes + + def prepare_representation(self, repre, + template_name, + existing_repres_by_name, + version, + instance_stagingdir, + instance): + + # pre-flight validations + if repre["ext"].startswith("."): + raise ValueError("Extension must not start with a dot '.': " + "{}".format(repre["ext"])) + + if repre.get("transfers"): + raise ValueError("Representation is not allowed to have transfers" + "data before integration. They are computed in " + "the integrator" + "Got: {}".format(repre["transfers"])) + + # create template data for Anatomy + template_data = copy.deepcopy(instance.data["anatomyData"]) + + # required representation keys + files = repre['files'] + template_data["representation"] = repre["name"] + template_data["ext"] = repre["ext"] + + # optionals + # retrieve additional anatomy data from representation if exists + for key, anatomy_key in { + # Representation Key: Anatomy data key + "resolutionWidth": "resolution_width", + "resolutionHeight": "resolution_height", + "fps": "fps", + "outputName": "output", + "originalBasename": "originalBasename" + }.items(): + # Allow to take value from representation + # if not found also consider instance.data + if key in repre: + value = repre[key] + elif key in instance.data: + value = instance.data[key] + else: + continue + template_data[anatomy_key] = value + + if repre.get('stagingDir'): + stagingdir = repre['stagingDir'] + else: + # Fall back to instance staging dir if not explicitly + # set for representation in the instance + self.log.debug("Representation uses instance staging dir: " + "{}".format(instance_stagingdir)) + stagingdir = instance_stagingdir + if not stagingdir: + raise ValueError("No staging directory set for representation: " + "{}".format(repre)) + + self.log.debug("Anatomy template name: {}".format(template_name)) + anatomy = instance.context.data['anatomy'] + template = os.path.normpath(anatomy.templates[template_name]["path"]) + + is_udim = bool(repre.get("udim")) + is_sequence_representation = isinstance(files, (list, tuple)) + if is_sequence_representation: + # Collection of files (sequence) + assert not any(os.path.isabs(fname) for fname in files), ( + "Given file names contain full paths" + ) + + src_collection = assemble(files) + + # If the representation has `frameStart` set it renumbers the + # frame indices of the published collection. It will start from + # that `frameStart` index instead. Thus if that frame start + # differs from the collection we want to shift the destination + # frame indices from the source collection. + destination_indexes = list(src_collection.indexes) + destination_padding = len(get_first_frame_padded(src_collection)) + if repre.get("frameStart") is not None and not is_udim: + index_frame_start = int(repre.get("frameStart")) + + render_template = anatomy.templates[template_name] + # todo: should we ALWAYS manage the frame padding even when not + # having `frameStart` set? + frame_start_padding = int( + render_template.get( + "frame_padding", + render_template.get("padding") + ) + ) + + # Shift destination sequence to the start frame + src_start_frame = next(iter(src_collection.indexes)) + shift = index_frame_start - src_start_frame + if shift: + destination_indexes = [ + frame + shift for frame in destination_indexes + ] + destination_padding = frame_start_padding + + # To construct the destination template with anatomy we require + # a Frame or UDIM tile set for the template data. We use the first + # index of the destination for that because that could've shifted + # from the source indexes, etc. + first_index_padded = get_frame_padded(frame=destination_indexes[0], + padding=destination_padding) + if is_udim: + # UDIM representations handle ranges in a different manner + template_data["udim"] = first_index_padded + else: + template_data["frame"] = first_index_padded + + # Construct destination collection from template + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled[template_name]["path"] + repre_context = template_filled.used_values + self.log.debug("Template filled: {}".format(str(template_filled))) + dst_collection = assemble([os.path.normpath(template_filled)]) + + # Update the destination indexes and padding + dst_collection.indexes.clear() + dst_collection.indexes.update(set(destination_indexes)) + dst_collection.padding = destination_padding + assert ( + len(src_collection.indexes) == len(dst_collection.indexes) + ), "This is a bug" + + # Multiple file transfers + transfers = [] + for src_file_name, dst in zip(src_collection, dst_collection): + src = os.path.join(stagingdir, src_file_name) + transfers.append((src, dst)) + + else: + # Single file + fname = files + assert not os.path.isabs(fname), ( + "Given file name is a full path" + ) + + # Manage anatomy template data + template_data.pop("frame", None) + if is_udim: + template_data["udim"] = repre["udim"][0] + + # Construct destination filepath from template + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled[template_name]["path"] + repre_context = template_filled.used_values + dst = os.path.normpath(template_filled) + + # Single file transfer + src = os.path.join(stagingdir, fname) + transfers = [(src, dst)] + + # todo: Are we sure the assumption each representation + # ends up in the same folder is valid? + if not instance.data.get("publishDir"): + instance.data["publishDir"] = ( + anatomy_filled + [template_name] + ["folder"] + ) + + for key in self.db_representation_context_keys: + # Also add these values to the context even if not used by the + # destination template + value = template_data.get(key) + if not value: + continue + repre_context[key] = template_data[key] + + # Explicitly store the full list even though template data might + # have a different value because it uses just a single udim tile + if repre.get("udim"): + repre_context["udim"] = repre.get("udim") # store list + + # Use previous representation's id if there is a name match + existing = existing_repres_by_name.get(repre["name"].lower()) + if existing: + repre_id = existing["_id"] + else: + repre_id = ObjectId() + + # Backwards compatibility: + # Store first transferred destination as published path data + # todo: can we remove this? + # todo: We shouldn't change data that makes its way back into + # instance.data[] until we know the publish actually succeeded + # otherwise `published_path` might not actually be valid? + published_path = transfers[0][1] + repre["published_path"] = published_path # Backwards compatibility + + # todo: `repre` is not the actual `representation` entity + # we should simplify/clarify difference between data above + # and the actual representation entity for the database + data = repre.get("data", {}) + data.update({'path': published_path, 'template': template}) + representation = { + "_id": repre_id, + "schema": "openpype:representation-2.0", + "type": "representation", + "parent": version["_id"], + "name": repre['name'], + "data": data, + + # Imprint shortcut to context for performance reasons. + "context": repre_context + } + + # todo: simplify/streamline which additional data makes its way into + # the representation context + if repre.get("outputName"): + representation["context"]["output"] = repre['outputName'] + + if is_sequence_representation and repre.get("frameStart") is not None: + representation['context']['frame'] = template_data["frame"] + + return { + "representation": representation, + "anatomy_data": template_data, + "transfers": transfers, + # todo: avoid the need for 'published_files' used by Integrate Hero + # backwards compatibility + "published_files": [transfer[1] for transfer in transfers] + } + + def create_version_data(self, instance): + """Create the data dictionary for the version + + Args: + instance: the current instance being published + + Returns: + dict: the required information for version["data"] + """ + + context = instance.context + + # create relative source path for DB + if "source" in instance.data: + source = instance.data["source"] + else: + source = context.data["currentFile"] + anatomy = instance.context.data["anatomy"] + source = self.get_rootless_path(anatomy, source) + self.log.debug("Source: {}".format(source)) + + version_data = { + "families": get_instance_families(instance), + "time": context.data["time"], + "author": context.data["user"], + "source": source, + "comment": context.data.get("comment"), + "machine": context.data.get("machine"), + "fps": instance.data.get("fps", context.data.get("fps")) + } + + # todo: preferably we wouldn't need this "if dict" etc. logic and + # instead be able to rely what the input value is if it's set. + intent_value = context.data.get("intent") + if intent_value and isinstance(intent_value, dict): + intent_value = intent_value.get("value") + + if intent_value: + version_data["intent"] = intent_value + + # Include optional data if present in + optionals = [ + "frameStart", "frameEnd", "step", "handles", + "handleEnd", "handleStart", "sourceHashes" + ] + for key in optionals: + if key in instance.data: + version_data[key] = instance.data[key] + + # Include instance.data[versionData] directly + version_data_instance = instance.data.get('versionData') + if version_data_instance: + version_data.update(version_data_instance) + + return version_data + + def get_template_name(self, instance): + """Return anatomy template name to use for integration""" + # Define publish template name from profiles + filter_criteria = self.get_profile_filter_criteria(instance) + profile = filter_profiles(self.template_name_profiles, + filter_criteria, + logger=self.log) + if profile: + return profile["template_name"] + else: + return self.default_template_name + + def get_profile_filter_criteria(self, instance): + """Return filter criteria for `filter_profiles`""" + # Anatomy data is pre-filled by Collectors + anatomy_data = instance.data["anatomyData"] + + # Task can be optional in anatomy data + task = anatomy_data.get("task", {}) + + # Return filter criteria + return { + "families": anatomy_data["family"], + "tasks": task.get("name"), + "hosts": anatomy_data["app"], + "task_types": task.get("type") + } + + def get_rootless_path(self, anatomy, path): + """Returns, if possible, path without absolute portion from root + (eg. 'c:\' or '/opt/..') + + This information is platform dependent and shouldn't be captured. + Example: + 'c:/projects/MyProject1/Assets/publish...' > + '{root}/MyProject1/Assets...' + + Args: + anatomy: anatomy part from instance + path: path (absolute) + Returns: + path: modified path if possible, or unmodified path + + warning logged + """ + success, rootless_path = anatomy.find_root_template_from_path(path) + if success: + path = rootless_path + else: + self.log.warning(( + "Could not find root path for remapping \"{}\"." + " This may cause issues on farm." + ).format(path)) + return path + + def get_files_info(self, destinations, sites, anatomy): + """Prepare 'files' info portion for representations. + + Arguments: + destinations (list): List of transferred file destinations + sites (list): array of published locations + anatomy: anatomy part from instance + Returns: + output_resources: array of dictionaries to be added to 'files' key + in representation + """ + file_infos = [] + for file_path in destinations: + file_info = self.prepare_file_info(file_path, anatomy, sites=sites) + file_infos.append(file_info) + return file_infos + + def prepare_file_info(self, path, anatomy, sites): + """ Prepare information for one file (asset or resource) + + Arguments: + path: destination url of published file + anatomy: anatomy part from instance + sites: array of published locations, + [ {'name':'studio', 'created_dt':date} by default + keys expected ['studio', 'site1', 'gdrive1'] + + Returns: + dict: file info dictionary + """ + return { + "_id": ObjectId(), + "path": self.get_rootless_path(anatomy, path), + "size": os.path.getsize(path), + "hash": openpype.api.source_hash(path), + "sites": sites + } From fd2d07e94c0fb34730547c396e09ddc314b56983 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Jul 2022 09:22:29 +0200 Subject: [PATCH 069/124] Revert integrator to latest develop --- openpype/plugins/publish/integrate_new.py | 1710 +++++++++++++-------- 1 file changed, 1088 insertions(+), 622 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index a07e8a1e0f..4c14c17dae 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -1,111 +1,63 @@ import os +from os.path import getsize import logging import sys import copy import clique +import errno import six +import re +import shutil +from collections import deque, defaultdict +from datetime import datetime from bson.objectid import ObjectId -from pymongo import DeleteMany, ReplaceOne, InsertOne, UpdateOne +from pymongo import DeleteOne, InsertOne import pyblish.api import openpype.api -from openpype.modules import ModulesManager from openpype.lib.profiles_filtering import filter_profiles -from openpype.lib.file_transaction import FileTransaction +from openpype.lib import ( + prepare_template_data, + create_hard_link, + StringTemplate, + TemplateUnsolved +) from openpype.pipeline import legacy_io +# this is needed until speedcopy for linux is fixed +if sys.platform == "win32": + from speedcopy import copyfile +else: + from shutil import copyfile + log = logging.getLogger(__name__) -def assemble(files): - """Convenience `clique.assemble` wrapper for files of a single collection. - - Unlike `clique.assemble` this wrapper does not allow more than a single - Collection nor any remainder files. Errors will be raised when not only - a single collection is assembled. - - Returns: - clique.Collection: A single sequence Collection - - Raises: - ValueError: Error is raised when files do not result in a single - collected Collection. - - """ - # todo: move this to lib? - # Get the sequence as a collection. The files must be of a single - # sequence and have no remainder outside of the collections. - patterns = [clique.PATTERNS["frames"]] - collections, remainder = clique.assemble(files, - minimum_items=1, - patterns=patterns) - if not collections: - raise ValueError("No collections found in files: " - "{}".format(files)) - if remainder: - raise ValueError("Files found not detected as part" - " of a sequence: {}".format(remainder)) - if len(collections) > 1: - raise ValueError("Files in sequence are not part of a" - " single sequence collection: " - "{}".format(collections)) - return collections[0] - - -def get_instance_families(instance): - """Get all families of the instance""" - # todo: move this to lib? - family = instance.data.get("family") - families = [] - if family: - families.append(family) - - for _family in (instance.data.get("families") or []): - if _family not in families: - families.append(_family) - - return families - - -def get_frame_padded(frame, padding): - """Return frame number as string with `padding` amount of padded zeros""" - return "{frame:0{padding}d}".format(padding=padding, frame=frame) - - -def get_first_frame_padded(collection): - """Return first frame as padded number from `clique.Collection`""" - start_frame = next(iter(collection.indexes)) - return get_frame_padded(start_frame, padding=collection.padding) - - -def bulk_write(writes): - """Convenience function to bulk write into active project database""" - project = legacy_io.Session["AVALON_PROJECT"] - return legacy_io._database[project].bulk_write(writes) - - class IntegrateAssetNew(pyblish.api.InstancePlugin): - """Register publish in the database and transfer files to destinations. + """Resolve any dependency issues - Steps: - 1) Register the subset and version - 2) Transfer the representation files to the destination - 3) Register the representation + This plug-in resolves any paths which, if not updated might break + the published file. - Requires: - instance.data['representations'] - must be a list and each member - must be a dictionary with following data: - 'files': list of filenames for sequence, string for single file. - Only the filename is allowed, without the folder path. - 'stagingDir': "path/to/folder/with/files" - 'name': representation name (usually the same as extension) - 'ext': file extension - optional data - "frameStart" - "frameEnd" - 'fps' - "data": additional metadata for each representation. + The order of families is important, when working with lookdev you want to + first publish the texture, update the texture paths in the nodes and then + publish the shading network. Same goes for file dependent assets. + + Requirements for instance to be correctly integrated + + instance.data['representations'] - must be a list and each member + must be a dictionary with following data: + 'files': list of filenames for sequence, string for single file. + Only the filename is allowed, without the folder path. + 'stagingDir': "path/to/folder/with/files" + 'name': representation name (usually the same as extension) + 'ext': file extension + optional data + "frameStart" + "frameEnd" + 'fps' + "data": additional metadata for each representation. """ label = "Integrate Asset New" @@ -140,6 +92,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "source", "matchmove", "image", + "source", "assembly", "fbx", "textures", @@ -156,51 +109,157 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "usd", "staticMesh", "skeletalMesh", - "usdComposition", - "usdOverride", + "mvLook", + "mvUsd", + "mvUsdComposition", + "mvUsdOverride", "simpleUnrealTexture" ] - exclude_families = ["clip", "render.farm"] - default_template_name = "publish" - - # Representation context keys that should always be written to - # the database even if not used by the destination template + exclude_families = ["render.farm"] db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", - "family", "hierarchy", "username" + "family", "hierarchy", "task", "username" ] + default_template_name = "publish" + + # suffix to denote temporary files, use without '.' + TMP_FILE_EXT = 'tmp' + + # file_url : file_size of all published and uploaded files + integrated_file_sizes = {} # Attributes set by settings template_name_profiles = None + subset_grouping_profiles = None def process(self, instance): + for ef in self.exclude_families: + if ( + instance.data["family"] == ef or + ef in instance.data["families"]): + self.log.debug("Excluded family '{}' in '{}' or {}".format( + ef, instance.data["family"], instance.data["families"])) + return - # Exclude instances that also contain families from exclude families - families = set(get_instance_families(instance)) - exclude = families & set(self.exclude_families) - if exclude: - self.log.debug("Instance not integrated due to exclude " - "families found: {}".format(", ".join(exclude))) + # instance should be published on a farm + if instance.data.get("farm"): return - file_transactions = FileTransaction(log=self.log) + # Prepare repsentations that should be integrated + repres = instance.data.get("representations") + # Raise error if instance don't have any representations + if not repres: + raise ValueError( + "Instance {} has no files to transfer".format( + instance.data["family"] + ) + ) + + # Validate type of stored representations + if not isinstance(repres, (list, tuple)): + raise TypeError( + "Instance 'files' must be a list, got: {0} {1}".format( + str(type(repres)), str(repres) + ) + ) + + # Filter representations + filtered_repres = [] + for repre in repres: + if "delete" in repre.get("tags", []): + continue + filtered_repres.append(repre) + + # Skip instance if there are not representations to integrate + # all representations should not be integrated + if not filtered_repres: + self.log.warning(( + "Skipping, there are no representations" + " to integrate for instance {}" + ).format(instance.data["family"])) + return + + self.integrated_file_sizes = {} try: - self.register(instance, file_transactions) + self.register(instance, filtered_repres) + self.log.info("Integrated Asset in to the database ...") + self.log.info("instance.data: {}".format(instance.data)) + self.handle_destination_files(self.integrated_file_sizes, + 'finalize') except Exception: # clean destination - # todo: preferably we'd also rollback *any* changes to the database - file_transactions.rollback() self.log.critical("Error when registering", exc_info=True) + self.handle_destination_files(self.integrated_file_sizes, 'remove') six.reraise(*sys.exc_info()) - # Finalizing can't rollback safely so no use for moving it to - # the try, except. - file_transactions.finalize() + def register(self, instance, repres): + # Required environment variables + anatomy_data = instance.data["anatomyData"] - def register(self, instance, file_transactions): + legacy_io.install() - instance_stagingdir = instance.data.get("stagingDir") - if not instance_stagingdir: + context = instance.context + + project_entity = instance.data["projectEntity"] + + context_asset_name = None + context_asset_doc = context.data.get("assetEntity") + if context_asset_doc: + context_asset_name = context_asset_doc["name"] + + asset_name = instance.data["asset"] + asset_entity = instance.data.get("assetEntity") + if not asset_entity or asset_entity["name"] != context_asset_name: + asset_entity = legacy_io.find_one({ + "type": "asset", + "name": asset_name, + "parent": project_entity["_id"] + }) + assert asset_entity, ( + "No asset found by the name \"{0}\" in project \"{1}\"" + ).format(asset_name, project_entity["name"]) + + instance.data["assetEntity"] = asset_entity + + # update anatomy data with asset specific keys + # - name should already been set + hierarchy = "" + parents = asset_entity["data"]["parents"] + if parents: + hierarchy = "/".join(parents) + anatomy_data["hierarchy"] = hierarchy + + # Make sure task name in anatomy data is same as on instance.data + asset_tasks = ( + asset_entity.get("data", {}).get("tasks") + ) or {} + task_name = instance.data.get("task") + if task_name: + task_info = asset_tasks.get(task_name) or {} + task_type = task_info.get("type") + + project_task_types = project_entity["config"]["tasks"] + task_code = project_task_types.get(task_type, {}).get("short_name") + anatomy_data["task"] = { + "name": task_name, + "type": task_type, + "short": task_code + } + + elif "task" in anatomy_data: + # Just set 'task_name' variable to context task + task_name = anatomy_data["task"]["name"] + task_type = anatomy_data["task"]["type"] + + else: + task_name = None + task_type = None + + # Fill family in anatomy data + anatomy_data["family"] = instance.data.get("family") + + stagingdir = instance.data.get("stagingDir") + if not stagingdir: self.log.info(( "{0} is missing reference to staging directory." " Will try to get it from representation." @@ -208,515 +267,718 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): else: self.log.debug( - "Establishing staging directory " - "@ {0}".format(instance_stagingdir) + "Establishing staging directory @ {0}".format(stagingdir) ) - # Ensure at least one representation is set up for registering. - repres = instance.data.get("representations") - assert repres, "Instance has no representations data" - assert isinstance(repres, (list, tuple)), ( - "Instance 'representations' must be a list, got: {0} {1}".format( - str(type(repres)), str(repres) - ) + subset = self.get_subset(asset_entity, instance) + instance.data["subsetEntity"] = subset + + version_number = instance.data["version"] + self.log.debug("Next version: v{}".format(version_number)) + + version_data = self.create_version_data(context, instance) + + version_data_instance = instance.data.get('versionData') + if version_data_instance: + version_data.update(version_data_instance) + + # TODO rename method from `create_version` to + # `prepare_version` or similar... + version = self.create_version( + subset=subset, + version_number=version_number, + data=version_data ) - template_name = self.get_template_name(instance) + self.log.debug("Creating version ...") - subset, subset_writes = self.prepare_subset(instance) - version, version_writes = self.prepare_version(instance, subset) + new_repre_names_low = [ + _repre["name"].lower() + for _repre in repres + ] + + existing_version = legacy_io.find_one({ + 'type': 'version', + 'parent': subset["_id"], + 'name': version_number + }) + + if existing_version is None: + version_id = legacy_io.insert_one(version).inserted_id + else: + # Check if instance have set `append` mode which cause that + # only replicated representations are set to archive + append_repres = instance.data.get("append", False) + + # Update version data + # TODO query by _id and + legacy_io.update_many({ + 'type': 'version', + 'parent': subset["_id"], + 'name': version_number + }, { + '$set': version + }) + version_id = existing_version['_id'] + + # Find representations of existing version and archive them + current_repres = list(legacy_io.find({ + "type": "representation", + "parent": version_id + })) + bulk_writes = [] + for repre in current_repres: + if append_repres: + # archive only duplicated representations + if repre["name"].lower() not in new_repre_names_low: + continue + # Representation must change type, + # `_id` must be stored to other key and replaced with new + # - that is because new representations should have same ID + repre_id = repre["_id"] + bulk_writes.append(DeleteOne({"_id": repre_id})) + + repre["orig_id"] = repre_id + repre["_id"] = ObjectId() + repre["type"] = "archived_representation" + bulk_writes.append(InsertOne(repre)) + + # bulk updates + if bulk_writes: + project_name = legacy_io.Session["AVALON_PROJECT"] + legacy_io.database[project_name].bulk_write( + bulk_writes + ) + + version = legacy_io.find_one({"_id": version_id}) instance.data["versionEntity"] = version - # Get existing representations (if any) - existing_repres_by_name = { - repres["name"].lower(): repres for repres in legacy_io.find( - { - "parent": version["_id"], - "type": "representation" - }, - # Only care about id and name of existing representations - projection={"_id": True, "name": True} - ) + existing_repres = list(legacy_io.find({ + "parent": version_id, + "type": "archived_representation" + })) + + instance.data['version'] = version['name'] + + intent_value = instance.context.data.get("intent") + if intent_value and isinstance(intent_value, dict): + intent_value = intent_value.get("value") + + if intent_value: + anatomy_data["intent"] = intent_value + + anatomy = instance.context.data['anatomy'] + + # Find the representations to transfer amongst the files + # Each should be a single representation (as such, a single extension) + representations = [] + destination_list = [] + + orig_transfers = [] + if 'transfers' not in instance.data: + instance.data['transfers'] = [] + else: + orig_transfers = list(instance.data['transfers']) + + family = self.main_family_from_instance(instance) + + key_values = { + "families": family, + "tasks": task_name, + "hosts": instance.context.data["hostName"], + "task_types": task_type } - - # Prepare all representations - prepared_representations = [] - for repre in instance.data["representations"]: - - if "delete" in repre.get("tags", []): - self.log.debug("Skipping representation marked for deletion: " - "{}".format(repre)) - continue - - # todo: reduce/simplify what is returned from this function - prepared = self.prepare_representation(repre, - template_name, - existing_repres_by_name, - version, - instance_stagingdir, - instance) - - for src, dst in prepared["transfers"]: - # todo: add support for hardlink transfers - file_transactions.add(src, dst) - - prepared_representations.append(prepared) - - if not prepared_representations: - # Even though we check `instance.data["representations"]` earlier - # this could still happen if all representations were tagged with - # "delete" and thus are skipped for integration - raise RuntimeError("No representations prepared to publish.") - - # Each instance can also have pre-defined transfers not explicitly - # part of a representation - like texture resources used by a - # .ma representation. Those destination paths are pre-defined, etc. - # todo: should we move or simplify this logic? - resource_destinations = set() - for src, dst in instance.data.get("transfers", []): - file_transactions.add(src, dst, mode=FileTransaction.MODE_COPY) - resource_destinations.add(os.path.abspath(dst)) - for src, dst in instance.data.get("hardlinks", []): - file_transactions.add(src, dst, mode=FileTransaction.MODE_HARDLINK) - resource_destinations.add(os.path.abspath(dst)) - - # Bulk write to the database - # We write the subset and version to the database before the File - # Transaction to reduce the chances of another publish trying to - # publish to the same version number since that chance can greatly - # increase if the file transaction takes a long time. - bulk_write(subset_writes + version_writes) - self.log.info("Subset {subset[name]} and Version {version[name]} " - "written to database..".format(subset=subset, - version=version)) - - # Process all file transfers of all integrations now - self.log.debug("Integrating source files to destination ...") - file_transactions.process() - self.log.debug("Backed up existing files: " - "{}".format(file_transactions.backups)) - self.log.debug("Transferred files: " - "{}".format(file_transactions.transferred)) - self.log.debug("Retrieving Representation Site Sync information ...") - - # Get the accessible sites for Site Sync - manager = ModulesManager() - sync_server_module = manager.modules_by_name["sync_server"] - sites = sync_server_module.compute_resource_sync_sites( - project_name=instance.data["projectEntity"]["name"] + profile = filter_profiles( + self.template_name_profiles, + key_values, + logger=self.log ) - self.log.debug("Sync Server Sites: {}".format(sites)) - # Compute the resource file infos once (files belonging to the - # version instance instead of an individual representation) so - # we can re-use those file infos per representation - anatomy = instance.context.data["anatomy"] - resource_file_infos = self.get_files_info(resource_destinations, - sites=sites, - anatomy=anatomy) + template_name = "publish" + if profile: + template_name = profile["template_name"] - # Finalize the representations now the published files are integrated - # Get 'files' info for representations and its attached resources - representation_writes = [] - new_repre_names_low = set() - for prepared in prepared_representations: - representation = prepared["representation"] - transfers = prepared["transfers"] - destinations = [dst for src, dst in transfers] + published_representations = {} + for idx, repre in enumerate(repres): + published_files = [] + + # create template data for Anatomy + template_data = copy.deepcopy(anatomy_data) + if intent_value is not None: + template_data["intent"] = intent_value + + resolution_width = repre.get("resolutionWidth") + resolution_height = repre.get("resolutionHeight") + fps = instance.data.get("fps") + + if resolution_width: + template_data["resolution_width"] = resolution_width + if resolution_width: + template_data["resolution_height"] = resolution_height + if resolution_width: + template_data["fps"] = fps + + if "originalBasename" in instance.data: + template_data.update({ + "originalBasename": instance.data.get("originalBasename") + }) + + files = repre['files'] + if repre.get('stagingDir'): + stagingdir = repre['stagingDir'] + + if repre.get("outputName"): + template_data["output"] = repre['outputName'] + + template_data["representation"] = repre["name"] + + ext = repre["ext"] + if ext.startswith("."): + self.log.warning(( + "Implementaion warning: <\"{}\">" + " Representation's extension stored under \"ext\" key " + " started with dot (\"{}\")." + ).format(repre["name"], ext)) + ext = ext[1:] + repre["ext"] = ext + template_data["ext"] = ext + + self.log.info(template_name) + template = os.path.normpath( + anatomy.templates[template_name]["path"]) + + sequence_repre = isinstance(files, list) + repre_context = None + if sequence_repre: + self.log.debug( + "files: {}".format(files)) + src_collections, remainder = clique.assemble(files) + self.log.debug( + "src_tail_collections: {}".format(str(src_collections))) + src_collection = src_collections[0] + + # Assert that each member has identical suffix + src_head = src_collection.format("{head}") + src_tail = src_collection.format("{tail}") + + # fix dst_padding + valid_files = [x for x in files if src_collection.match(x)] + padd_len = len( + valid_files[0].replace(src_head, "").replace(src_tail, "") + ) + src_padding_exp = "%0{}d".format(padd_len) + + test_dest_files = list() + for i in [1, 2]: + template_data["representation"] = repre['ext'] + if not repre.get("udim"): + template_data["frame"] = src_padding_exp % i + else: + template_data["udim"] = src_padding_exp % i + + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled[template_name]["path"] + if repre_context is None: + repre_context = template_filled.used_values + test_dest_files.append( + os.path.normpath(template_filled) + ) + if not repre.get("udim"): + template_data["frame"] = repre_context["frame"] + else: + template_data["udim"] = repre_context["udim"] + + self.log.debug( + "test_dest_files: {}".format(str(test_dest_files))) + + dst_collections, remainder = clique.assemble(test_dest_files) + dst_collection = dst_collections[0] + dst_head = dst_collection.format("{head}") + dst_tail = dst_collection.format("{tail}") + + index_frame_start = None + + # TODO use frame padding from right template group + if repre.get("frameStart") is not None: + frame_start_padding = int( + anatomy.templates["render"].get( + "frame_padding", + anatomy.templates["render"].get("padding") + ) + ) + + index_frame_start = int(repre.get("frameStart")) + + # exception for slate workflow + if index_frame_start and "slate" in instance.data["families"]: + index_frame_start -= 1 + + dst_padding_exp = src_padding_exp + dst_start_frame = None + collection_start = list(src_collection.indexes)[0] + for i in src_collection.indexes: + # TODO 1.) do not count padding in each index iteration + # 2.) do not count dst_padding from src_padding before + # index_frame_start check + frame_number = i - collection_start + src_padding = src_padding_exp % i + + src_file_name = "{0}{1}{2}".format( + src_head, src_padding, src_tail) + + dst_padding = src_padding_exp % frame_number + + if index_frame_start is not None: + dst_padding_exp = "%0{}d".format(frame_start_padding) + dst_padding = dst_padding_exp % (index_frame_start + frame_number) # noqa: E501 + elif repre.get("udim"): + dst_padding = int(i) + + dst = "{0}{1}{2}".format( + dst_head, + dst_padding, + dst_tail + ) + + self.log.debug("destination: `{}`".format(dst)) + src = os.path.join(stagingdir, src_file_name) + + self.log.debug("source: {}".format(src)) + instance.data["transfers"].append([src, dst]) + + published_files.append(dst) + + # for adding first frame into db + if not dst_start_frame: + dst_start_frame = dst_padding + + # Store used frame value to template data + if repre.get("frame"): + template_data["frame"] = dst_start_frame + + dst = "{0}{1}{2}".format( + dst_head, + dst_start_frame, + dst_tail + ) + repre['published_path'] = dst + + else: + # Single file + # _______ + # | |\ + # | | + # | | + # | | + # |_______| + # + template_data.pop("frame", None) + fname = files + assert not os.path.isabs(fname), ( + "Given file name is a full path" + ) + + template_data["representation"] = repre['ext'] + # Store used frame value to template data + if repre.get("udim"): + template_data["udim"] = repre["udim"][0] + src = os.path.join(stagingdir, fname) + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled[template_name]["path"] + repre_context = template_filled.used_values + dst = os.path.normpath(template_filled) + + instance.data["transfers"].append([src, dst]) + + published_files.append(dst) + repre['published_path'] = dst + self.log.debug("__ dst: {}".format(dst)) + + if not instance.data.get("publishDir"): + instance.data["publishDir"] = ( + anatomy_filled + [template_name] + ["folder"] + ) + if repre.get("udim"): + repre_context["udim"] = repre.get("udim") # store list + + repre["publishedFiles"] = published_files + + for key in self.db_representation_context_keys: + value = template_data.get(key) + if not value: + continue + repre_context[key] = template_data[key] + + # Use previous representation's id if there are any + repre_id = None + repre_name_low = repre["name"].lower() + for _repre in existing_repres: + # NOTE should we check lowered names? + if repre_name_low == _repre["name"]: + repre_id = _repre["orig_id"] + break + + # Create new id if existing representations does not match + if repre_id is None: + repre_id = ObjectId() + + data = repre.get("data") or {} + data.update({'path': dst, 'template': template}) + representation = { + "_id": repre_id, + "schema": "openpype:representation-2.0", + "type": "representation", + "parent": version_id, + "name": repre['name'], + "data": data, + "dependencies": instance.data.get("dependencies", "").split(), + + # Imprint shortcut to context + # for performance reasons. + "context": repre_context + } + + if repre.get("outputName"): + representation["context"]["output"] = repre['outputName'] + + if sequence_repre and repre.get("frameStart") is not None: + representation['context']['frame'] = ( + dst_padding_exp % int(repre.get("frameStart")) + ) + + # any file that should be physically copied is expected in + # 'transfers' or 'hardlinks' + if instance.data.get('transfers', False) or \ + instance.data.get('hardlinks', False): + # could throw exception, will be caught in 'process' + # all integration to DB is being done together lower, + # so no rollback needed + self.log.debug("Integrating source files to destination ...") + self.integrated_file_sizes.update(self.integrate(instance)) + self.log.debug("Integrated files {}". + format(self.integrated_file_sizes)) + + # get 'files' info for representation and all attached resources + self.log.debug("Preparing files information ...") representation["files"] = self.get_files_info( - destinations, sites=sites, anatomy=anatomy - ) + instance, + self.integrated_file_sizes) - # Add the version resource file infos to each representation - representation["files"] += resource_file_infos + self.log.debug("__ representation: {}".format(representation)) + destination_list.append(dst) + self.log.debug("__ destination_list: {}".format(destination_list)) + instance.data['destination_list'] = destination_list + representations.append(representation) + published_representations[repre_id] = { + "representation": representation, + "anatomy_data": template_data, + "published_files": published_files + } + self.log.debug("__ representations: {}".format(representations)) + # reset transfers for next representation + # instance.data['transfers'] is used as a global variable + # in current codebase + instance.data['transfers'] = list(orig_transfers) - # Set up representation for writing to the database. Since - # we *might* be overwriting an existing entry if the version - # already existed we'll use ReplaceOnce with `upsert=True` - representation_writes.append(ReplaceOne( - filter={"_id": representation["_id"]}, - replacement=representation, - upsert=True - )) + # Remove old representations if there are any (before insertion of new) + if existing_repres: + repre_ids_to_remove = [] + for repre in existing_repres: + repre_ids_to_remove.append(repre["_id"]) + legacy_io.delete_many({"_id": {"$in": repre_ids_to_remove}}) - new_repre_names_low.add(representation["name"].lower()) + for rep in instance.data["representations"]: + self.log.debug("__ rep: {}".format(rep)) - # Delete any existing representations that didn't get any new data - # if the instance is not set to append mode - if not instance.data.get("append", False): - delete_names = set() - for name, existing_repres in existing_repres_by_name.items(): - if name not in new_repre_names_low: - # We add the exact representation name because `name` is - # lowercase for name matching only and not in the database - delete_names.add(existing_repres["name"]) - if delete_names: - representation_writes.append(DeleteMany( - filter={ - "parent": version["_id"], - "name": {"$in": list(delete_names)} - } - )) + legacy_io.insert_many(representations) + instance.data["published_representations"] = ( + published_representations + ) + # self.log.debug("Representation: {}".format(representations)) + self.log.info("Registered {} items".format(len(representations))) - # Write representations to the database - bulk_write(representation_writes) + def integrate(self, instance): + """ Move the files. - # Backwards compatibility - # todo: can we avoid the need to store this? - instance.data["published_representations"] = { - p["representation"]["_id"]: p for p in prepared_representations - } + Through `instance.data["transfers"]` - self.log.info("Registered {} representations" - "".format(len(prepared_representations))) + Args: + instance: the instance to integrate + Returns: + integrated_file_sizes: dictionary of destination file url and + its size in bytes + """ + # store destination url and size for reporting and rollback + integrated_file_sizes = {} + transfers = list(instance.data.get("transfers", list())) + for src, dest in transfers: + if os.path.normpath(src) != os.path.normpath(dest): + dest = self.get_dest_temp_url(dest) + self.copy_file(src, dest) + # TODO needs to be updated during site implementation + integrated_file_sizes[dest] = os.path.getsize(dest) - def prepare_subset(self, instance): - asset = instance.data.get("assetEntity") + # Produce hardlinked copies + # Note: hardlink can only be produced between two files on the same + # server/disk and editing one of the two will edit both files at once. + # As such it is recommended to only make hardlinks between static files + # to ensure publishes remain safe and non-edited. + hardlinks = instance.data.get("hardlinks", list()) + for src, dest in hardlinks: + dest = self.get_dest_temp_url(dest) + self.log.debug("Hardlinking file ... {} -> {}".format(src, dest)) + if not os.path.exists(dest): + self.hardlink_file(src, dest) + + # TODO needs to be updated during site implementation + integrated_file_sizes[dest] = os.path.getsize(dest) + + return integrated_file_sizes + + def copy_file(self, src, dst): + """ Copy given source to destination + + Arguments: + src (str): the source file which needs to be copied + dst (str): the destination of the sourc file + Returns: + None + """ + src = os.path.normpath(src) + dst = os.path.normpath(dst) + self.log.debug("Copying file ... {} -> {}".format(src, dst)) + dirname = os.path.dirname(dst) + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + six.reraise(*sys.exc_info()) + + # copy file with speedcopy and check if size of files are simetrical + while True: + if not shutil._samefile(src, dst): + copyfile(src, dst) + else: + self.log.critical( + "files are the same {} to {}".format(src, dst) + ) + os.remove(dst) + try: + shutil.copyfile(src, dst) + self.log.debug("Copying files with shutil...") + except OSError as e: + self.log.critical("Cannot copy {} to {}".format(src, dst)) + self.log.critical(e) + six.reraise(*sys.exc_info()) + if str(getsize(src)) in str(getsize(dst)): + break + + def hardlink_file(self, src, dst): + dirname = os.path.dirname(dst) + + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + six.reraise(*sys.exc_info()) + + create_hard_link(src, dst) + + def get_subset(self, asset, instance): subset_name = instance.data["subset"] - self.log.debug("Subset: {}".format(subset_name)) - - # Get existing subset if it exists subset = legacy_io.find_one({ "type": "subset", "parent": asset["_id"], "name": subset_name }) - # Define subset data - data = { - "families": get_instance_families(instance) - } - - subset_group = instance.data.get("subsetGroup") - if subset_group: - data["subsetGroup"] = subset_group - - bulk_writes = [] if subset is None: - # Create a new subset self.log.info("Subset '%s' not found, creating ..." % subset_name) - subset = { - "_id": ObjectId(), + self.log.debug("families. %s" % instance.data.get('families')) + self.log.debug( + "families. %s" % type(instance.data.get('families'))) + + family = instance.data.get("family") + families = [] + if family: + families.append(family) + + for _family in (instance.data.get("families") or []): + if _family not in families: + families.append(_family) + + _id = legacy_io.insert_one({ "schema": "openpype:subset-3.0", "type": "subset", "name": subset_name, - "data": data, + "data": { + "families": families + }, "parent": asset["_id"] - } - bulk_writes.append(InsertOne(subset)) + }).inserted_id - else: - # Update existing subset data with new data and set in database. - # We also change the found subset in-place so we don't need to - # re-query the subset afterwards - subset["data"].update(data) - bulk_writes.append(UpdateOne( - {"type": "subset", "_id": subset["_id"]}, - {"$set": { - "data": subset["data"] - }} - )) + subset = legacy_io.find_one({"_id": _id}) - self.log.info("Prepared subset: {}".format(subset_name)) - return subset, bulk_writes + # QUESTION Why is changing of group and updating it's + # families in 'get_subset'? + self._set_subset_group(instance, subset["_id"]) - def prepare_version(self, instance, subset): + # Update families on subset. + families = [instance.data["family"]] + families.extend(instance.data.get("families", [])) + legacy_io.update_many( + {"type": "subset", "_id": ObjectId(subset["_id"])}, + {"$set": {"data.families": families}} + ) - version_number = instance.data["version"] + return subset - version = { - "schema": "openpype:version-3.0", - "type": "version", - "parent": subset["_id"], - "name": version_number, - "data": self.create_version_data(instance) + def _set_subset_group(self, instance, subset_id): + """ + Mark subset as belonging to group in DB. + + Uses Settings > Global > Publish plugins > IntegrateAssetNew + + Args: + instance (dict): processed instance + subset_id (str): DB's subset _id + + """ + # Fist look into instance data + subset_group = instance.data.get("subsetGroup") + if not subset_group: + subset_group = self._get_subset_group(instance) + + if subset_group: + legacy_io.update_many({ + 'type': 'subset', + '_id': ObjectId(subset_id) + }, {'$set': {'data.subsetGroup': subset_group}}) + + def _get_subset_group(self, instance): + """Look into subset group profiles set by settings. + + Attribute 'subset_grouping_profiles' is defined by OpenPype settings. + """ + # Skip if 'subset_grouping_profiles' is empty + if not self.subset_grouping_profiles: + return None + + # QUESTION + # - is there a chance that task name is not filled in anatomy + # data? + # - should we use context task in that case? + anatomy_data = instance.data["anatomyData"] + task_name = None + task_type = None + if "task" in anatomy_data: + task_name = anatomy_data["task"]["name"] + task_type = anatomy_data["task"]["type"] + filtering_criteria = { + "families": instance.data["family"], + "hosts": instance.context.data["hostName"], + "tasks": task_name, + "task_types": task_type } + matching_profile = filter_profiles( + self.subset_grouping_profiles, + filtering_criteria + ) + # Skip if there is not matchin profile + if not matching_profile: + return None - existing_version = legacy_io.find_one({ - 'type': 'version', - 'parent': subset["_id"], - 'name': version_number - }, projection={"_id": True}) + filled_template = None + template = matching_profile["template"] + fill_pairs = ( + ("family", filtering_criteria["families"]), + ("task", filtering_criteria["tasks"]), + ("host", filtering_criteria["hosts"]), + ("subset", instance.data["subset"]), + ("renderlayer", instance.data.get("renderlayer")) + ) + fill_pairs = prepare_template_data(fill_pairs) - if existing_version: - self.log.debug("Updating existing version ...") - version["_id"] = existing_version["_id"] - else: - self.log.debug("Creating new version ...") - version["_id"] = ObjectId() - - bulk_writes = [ReplaceOne( - filter={"_id": version["_id"]}, - replacement=version, - upsert=True - )] - - self.log.info("Prepared version: v{0:03d}".format(version["name"])) - - return version, bulk_writes - - def prepare_representation(self, repre, - template_name, - existing_repres_by_name, - version, - instance_stagingdir, - instance): - - # pre-flight validations - if repre["ext"].startswith("."): - raise ValueError("Extension must not start with a dot '.': " - "{}".format(repre["ext"])) - - if repre.get("transfers"): - raise ValueError("Representation is not allowed to have transfers" - "data before integration. They are computed in " - "the integrator" - "Got: {}".format(repre["transfers"])) - - # create template data for Anatomy - template_data = copy.deepcopy(instance.data["anatomyData"]) - - # required representation keys - files = repre['files'] - template_data["representation"] = repre["name"] - template_data["ext"] = repre["ext"] - - # optionals - # retrieve additional anatomy data from representation if exists - for key, anatomy_key in { - # Representation Key: Anatomy data key - "resolutionWidth": "resolution_width", - "resolutionHeight": "resolution_height", - "fps": "fps", - "outputName": "output", - "originalBasename": "originalBasename" - }.items(): - # Allow to take value from representation - # if not found also consider instance.data - if key in repre: - value = repre[key] - elif key in instance.data: - value = instance.data[key] - else: - continue - template_data[anatomy_key] = value - - if repre.get('stagingDir'): - stagingdir = repre['stagingDir'] - else: - # Fall back to instance staging dir if not explicitly - # set for representation in the instance - self.log.debug("Representation uses instance staging dir: " - "{}".format(instance_stagingdir)) - stagingdir = instance_stagingdir - if not stagingdir: - raise ValueError("No staging directory set for representation: " - "{}".format(repre)) - - self.log.debug("Anatomy template name: {}".format(template_name)) - anatomy = instance.context.data['anatomy'] - template = os.path.normpath(anatomy.templates[template_name]["path"]) - - is_udim = bool(repre.get("udim")) - is_sequence_representation = isinstance(files, (list, tuple)) - if is_sequence_representation: - # Collection of files (sequence) - assert not any(os.path.isabs(fname) for fname in files), ( - "Given file names contain full paths" + try: + filled_template = StringTemplate.format_strict_template( + template, fill_pairs ) + except (KeyError, TemplateUnsolved): + keys = [] + if fill_pairs: + keys = fill_pairs.keys() - src_collection = assemble(files) + msg = "Subset grouping failed. " \ + "Only {} are expected in Settings".format(','.join(keys)) + self.log.warning(msg) - # If the representation has `frameStart` set it renumbers the - # frame indices of the published collection. It will start from - # that `frameStart` index instead. Thus if that frame start - # differs from the collection we want to shift the destination - # frame indices from the source collection. - destination_indexes = list(src_collection.indexes) - destination_padding = len(get_first_frame_padded(src_collection)) - if repre.get("frameStart") is not None and not is_udim: - index_frame_start = int(repre.get("frameStart")) + return filled_template - render_template = anatomy.templates[template_name] - # todo: should we ALWAYS manage the frame padding even when not - # having `frameStart` set? - frame_start_padding = int( - render_template.get( - "frame_padding", - render_template.get("padding") - ) - ) - - # Shift destination sequence to the start frame - src_start_frame = next(iter(src_collection.indexes)) - shift = index_frame_start - src_start_frame - if shift: - destination_indexes = [ - frame + shift for frame in destination_indexes - ] - destination_padding = frame_start_padding - - # To construct the destination template with anatomy we require - # a Frame or UDIM tile set for the template data. We use the first - # index of the destination for that because that could've shifted - # from the source indexes, etc. - first_index_padded = get_frame_padded(frame=destination_indexes[0], - padding=destination_padding) - if is_udim: - # UDIM representations handle ranges in a different manner - template_data["udim"] = first_index_padded - else: - template_data["frame"] = first_index_padded - - # Construct destination collection from template - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] - repre_context = template_filled.used_values - self.log.debug("Template filled: {}".format(str(template_filled))) - dst_collection = assemble([os.path.normpath(template_filled)]) - - # Update the destination indexes and padding - dst_collection.indexes.clear() - dst_collection.indexes.update(set(destination_indexes)) - dst_collection.padding = destination_padding - assert ( - len(src_collection.indexes) == len(dst_collection.indexes) - ), "This is a bug" - - # Multiple file transfers - transfers = [] - for src_file_name, dst in zip(src_collection, dst_collection): - src = os.path.join(stagingdir, src_file_name) - transfers.append((src, dst)) - - else: - # Single file - fname = files - assert not os.path.isabs(fname), ( - "Given file name is a full path" - ) - - # Manage anatomy template data - template_data.pop("frame", None) - if is_udim: - template_data["udim"] = repre["udim"][0] - - # Construct destination filepath from template - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] - repre_context = template_filled.used_values - dst = os.path.normpath(template_filled) - - # Single file transfer - src = os.path.join(stagingdir, fname) - transfers = [(src, dst)] - - # todo: Are we sure the assumption each representation - # ends up in the same folder is valid? - if not instance.data.get("publishDir"): - instance.data["publishDir"] = ( - anatomy_filled - [template_name] - ["folder"] - ) - - for key in self.db_representation_context_keys: - # Also add these values to the context even if not used by the - # destination template - value = template_data.get(key) - if not value: - continue - repre_context[key] = template_data[key] - - # Explicitly store the full list even though template data might - # have a different value because it uses just a single udim tile - if repre.get("udim"): - repre_context["udim"] = repre.get("udim") # store list - - # Use previous representation's id if there is a name match - existing = existing_repres_by_name.get(repre["name"].lower()) - if existing: - repre_id = existing["_id"] - else: - repre_id = ObjectId() - - # Backwards compatibility: - # Store first transferred destination as published path data - # todo: can we remove this? - # todo: We shouldn't change data that makes its way back into - # instance.data[] until we know the publish actually succeeded - # otherwise `published_path` might not actually be valid? - published_path = transfers[0][1] - repre["published_path"] = published_path # Backwards compatibility - - # todo: `repre` is not the actual `representation` entity - # we should simplify/clarify difference between data above - # and the actual representation entity for the database - data = repre.get("data", {}) - data.update({'path': published_path, 'template': template}) - representation = { - "_id": repre_id, - "schema": "openpype:representation-2.0", - "type": "representation", - "parent": version["_id"], - "name": repre['name'], - "data": data, - - # Imprint shortcut to context for performance reasons. - "context": repre_context - } - - # todo: simplify/streamline which additional data makes its way into - # the representation context - if repre.get("outputName"): - representation["context"]["output"] = repre['outputName'] - - if is_sequence_representation and repre.get("frameStart") is not None: - representation['context']['frame'] = template_data["frame"] - - return { - "representation": representation, - "anatomy_data": template_data, - "transfers": transfers, - # todo: avoid the need for 'published_files' used by Integrate Hero - # backwards compatibility - "published_files": [transfer[1] for transfer in transfers] - } - - def create_version_data(self, instance): - """Create the data dictionary for the version + def create_version(self, subset, version_number, data=None): + """ Copy given source to destination Args: + subset (dict): the registered subset of the asset + version_number (int): the version number + + Returns: + dict: collection of data to create a version + """ + + return {"schema": "openpype:version-3.0", + "type": "version", + "parent": subset["_id"], + "name": version_number, + "data": data} + + def create_version_data(self, context, instance): + """Create the data collection for the version + + Args: + context: the current context instance: the current instance being published Returns: - dict: the required information for version["data"] + dict: the required information with instance.data as key """ - context = instance.context + families = [] + current_families = instance.data.get("families", list()) + instance_family = instance.data.get("family", None) + + if instance_family is not None: + families.append(instance_family) + families += current_families # create relative source path for DB - if "source" in instance.data: - source = instance.data["source"] - else: + source = instance.data.get("source") + if not source: source = context.data["currentFile"] anatomy = instance.context.data["anatomy"] source = self.get_rootless_path(anatomy, source) - self.log.debug("Source: {}".format(source)) + self.log.debug("Source: {}".format(source)) version_data = { - "families": get_instance_families(instance), + "families": families, "time": context.data["time"], "author": context.data["user"], "source": source, "comment": context.data.get("comment"), "machine": context.data.get("machine"), - "fps": instance.data.get("fps", context.data.get("fps")) + "fps": context.data.get( + "fps", instance.data.get("fps") + ) } - # todo: preferably we wouldn't need this "if dict" etc. logic and - # instead be able to rely what the input value is if it's set. - intent_value = context.data.get("intent") + intent_value = instance.context.data.get("intent") if intent_value and isinstance(intent_value, dict): intent_value = intent_value.get("value") @@ -732,58 +994,33 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if key in instance.data: version_data[key] = instance.data[key] - # Include instance.data[versionData] directly - version_data_instance = instance.data.get('versionData') - if version_data_instance: - version_data.update(version_data_instance) - return version_data - def get_template_name(self, instance): - """Return anatomy template name to use for integration""" - # Define publish template name from profiles - filter_criteria = self.get_profile_filter_criteria(instance) - profile = filter_profiles(self.template_name_profiles, - filter_criteria, - logger=self.log) - if profile: - return profile["template_name"] - else: - return self.default_template_name - - def get_profile_filter_criteria(self, instance): - """Return filter criteria for `filter_profiles`""" - # Anatomy data is pre-filled by Collectors - anatomy_data = instance.data["anatomyData"] - - # Task can be optional in anatomy data - task = anatomy_data.get("task", {}) - - # Return filter criteria - return { - "families": anatomy_data["family"], - "tasks": task.get("name"), - "hosts": anatomy_data["app"], - "task_types": task.get("type") - } + def main_family_from_instance(self, instance): + """Returns main family of entered instance.""" + family = instance.data.get("family") + if not family: + family = instance.data["families"][0] + return family def get_rootless_path(self, anatomy, path): - """Returns, if possible, path without absolute portion from root - (eg. 'c:\' or '/opt/..') - - This information is platform dependent and shouldn't be captured. - Example: - 'c:/projects/MyProject1/Assets/publish...' > - '{root}/MyProject1/Assets...' + """ Returns, if possible, path without absolute portion from host + (eg. 'c:\' or '/opt/..') + This information is host dependent and shouldn't be captured. + Example: + 'c:/projects/MyProject1/Assets/publish...' > + '{root}/MyProject1/Assets...' Args: - anatomy: anatomy part from instance - path: path (absolute) + anatomy: anatomy part from instance + path: path (absolute) Returns: - path: modified path if possible, or unmodified path - + warning logged + path: modified path if possible, or unmodified path + + warning logged """ - success, rootless_path = anatomy.find_root_template_from_path(path) + success, rootless_path = ( + anatomy.find_root_template_from_path(path) + ) if success: path = rootless_path else: @@ -793,40 +1030,269 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ).format(path)) return path - def get_files_info(self, destinations, sites, anatomy): - """Prepare 'files' info portion for representations. + def get_files_info(self, instance, integrated_file_sizes): + """ Prepare 'files' portion for attached resources and main asset. + Combining records from 'transfers' and 'hardlinks' parts from + instance. + All attached resources should be added, currently without + Context info. Arguments: - destinations (list): List of transferred file destinations - sites (list): array of published locations - anatomy: anatomy part from instance + instance: the current instance being published + integrated_file_sizes: dictionary of destination path (absolute) + and its file size Returns: output_resources: array of dictionaries to be added to 'files' key in representation """ - file_infos = [] - for file_path in destinations: - file_info = self.prepare_file_info(file_path, anatomy, sites=sites) - file_infos.append(file_info) - return file_infos + resources = list(instance.data.get("transfers", [])) + resources.extend(list(instance.data.get("hardlinks", []))) - def prepare_file_info(self, path, anatomy, sites): + self.log.debug("get_resource_files_info.resources:{}". + format(resources)) + + output_resources = [] + anatomy = instance.context.data["anatomy"] + for _src, dest in resources: + path = self.get_rootless_path(anatomy, dest) + dest = self.get_dest_temp_url(dest) + file_hash = openpype.api.source_hash(dest) + if self.TMP_FILE_EXT and \ + ',{}'.format(self.TMP_FILE_EXT) in file_hash: + file_hash = file_hash.replace(',{}'.format(self.TMP_FILE_EXT), + '') + + file_info = self.prepare_file_info(path, + integrated_file_sizes[dest], + file_hash, + instance=instance) + output_resources.append(file_info) + + return output_resources + + def get_dest_temp_url(self, dest): + """ Enhance destination path with TMP_FILE_EXT to denote temporary + file. + Temporary files will be renamed after successful registration + into DB and full copy to destination + + Arguments: + dest: destination url of published file (absolute) + Returns: + dest: destination path + '.TMP_FILE_EXT' + """ + if self.TMP_FILE_EXT and '.{}'.format(self.TMP_FILE_EXT) not in dest: + dest += '.{}'.format(self.TMP_FILE_EXT) + return dest + + def prepare_file_info(self, path, size=None, file_hash=None, + sites=None, instance=None): """ Prepare information for one file (asset or resource) Arguments: - path: destination url of published file - anatomy: anatomy part from instance - sites: array of published locations, - [ {'name':'studio', 'created_dt':date} by default - keys expected ['studio', 'site1', 'gdrive1'] - + path: destination url of published file (rootless) + size(optional): size of file in bytes + file_hash(optional): hash of file for synchronization validation + sites(optional): array of published locations, + [ {'name':'studio', 'created_dt':date} by default + keys expected ['studio', 'site1', 'gdrive1'] + instance(dict, optional): to get collected settings Returns: - dict: file info dictionary + rec: dictionary with filled info """ - return { + local_site = 'studio' # default + remote_site = None + always_accesible = [] + sync_project_presets = None + + rec = { "_id": ObjectId(), - "path": self.get_rootless_path(anatomy, path), - "size": os.path.getsize(path), - "hash": openpype.api.source_hash(path), - "sites": sites + "path": path } + if size: + rec["size"] = size + + if file_hash: + rec["hash"] = file_hash + + if sites: + rec["sites"] = sites + else: + system_sync_server_presets = ( + instance.context.data["system_settings"] + ["modules"] + ["sync_server"]) + log.debug("system_sett:: {}".format(system_sync_server_presets)) + + if system_sync_server_presets["enabled"]: + sync_project_presets = ( + instance.context.data["project_settings"] + ["global"] + ["sync_server"]) + + if sync_project_presets and sync_project_presets["enabled"]: + local_site, remote_site = self._get_sites(sync_project_presets) + + always_accesible = sync_project_presets["config"]. \ + get("always_accessible_on", []) + + already_attached_sites = {} + meta = {"name": local_site, "created_dt": datetime.now()} + rec["sites"] = [meta] + already_attached_sites[meta["name"]] = meta["created_dt"] + + if sync_project_presets and sync_project_presets["enabled"]: + if remote_site and \ + remote_site not in already_attached_sites.keys(): + # add remote + meta = {"name": remote_site.strip()} + rec["sites"].append(meta) + already_attached_sites[meta["name"]] = None + + # add alternative sites + rec, already_attached_sites = self._add_alternative_sites( + system_sync_server_presets, already_attached_sites, rec) + + # add skeleton for site where it should be always synced to + for always_on_site in set(always_accesible): + if always_on_site not in already_attached_sites.keys(): + meta = {"name": always_on_site.strip()} + rec["sites"].append(meta) + already_attached_sites[meta["name"]] = None + + log.debug("final sites:: {}".format(rec["sites"])) + + return rec + + def _get_sites(self, sync_project_presets): + """Returns tuple (local_site, remote_site)""" + local_site_id = openpype.api.get_local_site_id() + local_site = sync_project_presets["config"]. \ + get("active_site", "studio").strip() + + if local_site == 'local': + local_site = local_site_id + + remote_site = sync_project_presets["config"].get("remote_site") + + if remote_site == 'local': + remote_site = local_site_id + + return local_site, remote_site + + def _add_alternative_sites(self, + system_sync_server_presets, + already_attached_sites, + rec): + """Loop through all configured sites and add alternatives. + + See SyncServerModule.handle_alternate_site + """ + conf_sites = system_sync_server_presets.get("sites", {}) + + alt_site_pairs = self._get_alt_site_pairs(conf_sites) + + already_attached_keys = list(already_attached_sites.keys()) + for added_site in already_attached_keys: + real_created = already_attached_sites[added_site] + for alt_site in alt_site_pairs.get(added_site, []): + if alt_site in already_attached_sites.keys(): + continue + meta = {"name": alt_site} + # alt site inherits state of 'created_dt' + if real_created: + meta["created_dt"] = real_created + rec["sites"].append(meta) + already_attached_sites[meta["name"]] = real_created + + return rec, already_attached_sites + + def _get_alt_site_pairs(self, conf_sites): + """Returns dict of site and its alternative sites. + + If `site` has alternative site, it means that alt_site has 'site' as + alternative site + Args: + conf_sites (dict) + Returns: + (dict): {'site': [alternative sites]...} + """ + alt_site_pairs = defaultdict(list) + for site_name, site_info in conf_sites.items(): + alt_sites = set(site_info.get("alternative_sites", [])) + alt_site_pairs[site_name].extend(alt_sites) + + for alt_site in alt_sites: + alt_site_pairs[alt_site].append(site_name) + + for site_name, alt_sites in alt_site_pairs.items(): + sites_queue = deque(alt_sites) + while sites_queue: + alt_site = sites_queue.popleft() + + # safety against wrong config + # {"SFTP": {"alternative_site": "SFTP"} + if alt_site == site_name or alt_site not in alt_site_pairs: + continue + + for alt_alt_site in alt_site_pairs[alt_site]: + if ( + alt_alt_site != site_name + and alt_alt_site not in alt_sites + ): + alt_sites.append(alt_alt_site) + sites_queue.append(alt_alt_site) + + return alt_site_pairs + + def handle_destination_files(self, integrated_file_sizes, mode): + """ Clean destination files + Called when error happened during integrating to DB or to disk + OR called to rename uploaded files from temporary name to final to + highlight publishing in progress/broken + Used to clean unwanted files + + Arguments: + integrated_file_sizes: dictionary, file urls as keys, size as value + mode: 'remove' - clean files, + 'finalize' - rename files, + remove TMP_FILE_EXT suffix denoting temp file + """ + if integrated_file_sizes: + for file_url, _file_size in integrated_file_sizes.items(): + if not os.path.exists(file_url): + self.log.debug( + "File {} was not found.".format(file_url) + ) + continue + + try: + if mode == 'remove': + self.log.debug("Removing file {}".format(file_url)) + os.remove(file_url) + if mode == 'finalize': + new_name = re.sub( + r'\.{}$'.format(self.TMP_FILE_EXT), + '', + file_url + ) + + if os.path.exists(new_name): + self.log.debug( + "Overwriting file {} to {}".format( + file_url, new_name + ) + ) + shutil.copy(file_url, new_name) + os.remove(file_url) + else: + self.log.debug( + "Renaming file {} to {}".format( + file_url, new_name + ) + ) + os.rename(file_url, new_name) + except OSError: + self.log.error("Cannot {} file {}".format(mode, file_url), + exc_info=True) + six.reraise(*sys.exc_info()) From 271a829f6d441bcf26e6ddaf33510f984dc0c703 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Jul 2022 09:24:38 +0200 Subject: [PATCH 070/124] Remove duplicate source family --- openpype/plugins/publish/integrate_new.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 4c14c17dae..fd3cf8882d 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -92,7 +92,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "source", "matchmove", "image", - "source", "assembly", "fbx", "textures", From 148ac26bf961aa8e44ffcd453efbdbb0f4a8df75 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Jul 2022 09:28:00 +0200 Subject: [PATCH 071/124] Update USD families with latest develop --- openpype/plugins/publish/integrate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 6ad0849ff7..6253a3ec11 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -156,8 +156,10 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "usd", "staticMesh", "skeletalMesh", - "usdComposition", - "usdOverride", + "mvLook", + "mvUsd", + "mvUsdComposition", + "mvUsdOverride", "simpleUnrealTexture" ] exclude_families = ["clip", "render.farm"] From 035c4d2f93fd0a29ba8f6f1789a327878861284a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Jul 2022 09:30:07 +0200 Subject: [PATCH 072/124] Set up old vs. new integrator per host --- openpype/plugins/publish/integrate.py | 1 + openpype/plugins/publish/integrate_new.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 6253a3ec11..d098147603 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -110,6 +110,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): label = "Integrate Asset New" order = pyblish.api.IntegratorOrder + hosts = ["maya"] families = ["workfile", "pointcache", "camera", diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index fd3cf8882d..c9848abc14 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -62,6 +62,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): label = "Integrate Asset New" order = pyblish.api.IntegratorOrder + hosts = ["aftereffects", "blender", "celaction", "flame", "harmony", + "hiero", "houdini", "nuke", "photoshop", "resolve", + "standalonepublisher", "traypublisher", "tvpaint", "unreal", + "webpublisher"] families = ["workfile", "pointcache", "camera", From a3757636e7705b34699adff7e1e23f7ff57284d6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Jul 2022 09:31:53 +0200 Subject: [PATCH 073/124] Remove 'intent' context data override @iLLiCiTiT says: Intent should be a dictionary with "value" and "label", to be able tell if you want use value or label of the intent in templates. --- openpype/plugins/publish/collect_anatomy_context_data.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 8db9d0d3d7..0794adfb67 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -92,13 +92,5 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): } }) - # todo: some code actually expects the dict itself and others doesn't - # question: what should it be? - intent = context.data.get("intent") - if intent and isinstance(intent, dict): - intent = intent.get("value") - if intent: - context_data["intent"] = intent - self.log.info("Global anatomy Data collected") self.log.debug(json.dumps(context_data, indent=4)) From b4697b6e1a0cc778765d617b68d1e516ca7dcea9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Jul 2022 10:36:37 +0200 Subject: [PATCH 074/124] Refactor integrator labels --- openpype/plugins/publish/integrate.py | 2 +- openpype/plugins/publish/integrate_new.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index d098147603..5e86eb014a 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -108,7 +108,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "data": additional metadata for each representation. """ - label = "Integrate Asset New" + label = "Integrate Asset" order = pyblish.api.IntegratorOrder hosts = ["maya"] families = ["workfile", diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index c9848abc14..baa14b285c 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -60,7 +60,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "data": additional metadata for each representation. """ - label = "Integrate Asset New" + label = "Integrate Asset (legacy)" order = pyblish.api.IntegratorOrder hosts = ["aftereffects", "blender", "celaction", "flame", "harmony", "hiero", "houdini", "nuke", "photoshop", "resolve", From 45473c5a832b4db881ca328ad89324ada93ae0e5 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 13 Jul 2022 16:24:08 +0200 Subject: [PATCH 075/124] add host and families settings to integrators --- .../defaults/project_settings/global.json | 171 ++++++++++++++++++ .../schemas/schema_global_publish.json | 79 ++++++++ 2 files changed, 250 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 4e9b61100e..545c792d47 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -171,6 +171,177 @@ ] }, "IntegrateAssetNew": { + "hosts": [ + "aftereffects", + "blender", + "celaction", + "flame", + "fusion", + "harmony", + "hiero", + "houdini", + "nuke", + "photoshop", + "resolve", + "tvpaint", + "unreal", + "standalonepublisher", + "webpublisher" + ], + "families": [ + "workfile", + "pointcache", + "camera", + "animation", + "model", + "mayaAscii", + "mayaScene", + "setdress", + "layout", + "ass", + "vdbcache", + "scene", + "vrayproxy", + "vrayscene_layer", + "render", + "prerender", + "imagesequence", + "review", + "rendersetup", + "rig", + "plate", + "look", + "audio", + "yetiRig", + "yeticache", + "nukenodes", + "gizmo", + "source", + "matchmove", + "image", + "assembly", + "fbx", + "textures", + "action", + "harmony.template", + "harmony.palette", + "editorial", + "background", + "camerarig", + "redshiftproxy", + "effect", + "xgen", + "hda", + "usd", + "staticMesh", + "skeletalMesh", + "mvLook", + "mvUsd", + "mvUsdComposition", + "mvUsdOverride", + "simpleUnrealTexture" + ], + "template_name_profiles": [ + { + "families": [], + "hosts": [], + "task_types": [], + "tasks": [], + "template_name": "publish" + }, + { + "families": [ + "review", + "render", + "prerender" + ], + "hosts": [], + "task_types": [], + "tasks": [], + "template_name": "render" + }, + { + "families": [ + "simpleUnrealTexture" + ], + "hosts": [ + "standalonepublisher" + ], + "task_types": [], + "tasks": [], + "template_name": "simpleUnrealTexture" + }, + { + "families": [ + "staticMesh", + "skeletalMesh" + ], + "hosts": [ + "maya" + ], + "task_types": [], + "tasks": [], + "template_name": "maya2unreal" + } + ] + }, + "IntegrateAsset": { + "hosts": [ + "maya" + ], + "families": [ + "workfile", + "pointcache", + "camera", + "animation", + "model", + "mayaAscii", + "mayaScene", + "setdress", + "layout", + "ass", + "vdbcache", + "scene", + "vrayproxy", + "vrayscene_layer", + "render", + "prerender", + "imagesequence", + "review", + "rendersetup", + "rig", + "plate", + "look", + "audio", + "yetiRig", + "yeticache", + "nukenodes", + "gizmo", + "source", + "matchmove", + "image", + "assembly", + "fbx", + "textures", + "action", + "harmony.template", + "harmony.palette", + "editorial", + "background", + "camerarig", + "redshiftproxy", + "effect", + "xgen", + "hda", + "usd", + "staticMesh", + "skeletalMesh", + "mvLook", + "mvUsd", + "mvUsdComposition", + "mvUsdOverride", + "simpleUnrealTexture" + ], "template_name_profiles": [ { "families": [], diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index e368916cc9..71eed2e2de 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -587,6 +587,85 @@ "label": "IntegrateAssetNew", "is_group": true, "children": [ + { + "type": "list", + "key": "hosts", + "label": "Hosts", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "type": "list", + "key": "template_name_profiles", + "label": "Template name profiles", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "label", + "label": "" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "type": "hosts-enum", + "key": "hosts", + "label": "Hosts", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "template_name", + "label": "Template name" + } + ] + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "IntegrateAsset", + "label": "IntegrateAsset", + "is_group": true, + "children": [ + { + "type": "list", + "key": "hosts", + "label": "Hosts", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, { "type": "list", "key": "template_name_profiles", From b9be23496924fe4ac99e764b11a82db348ef0b3e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 15:19:07 +0200 Subject: [PATCH 076/124] removed default host used on deregister of host --- openpype/pipeline/context_tools.py | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index e719e46514..fd4dc6e3fd 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -240,29 +240,7 @@ def registered_host(): def deregister_host(): - _registered_host["_"] = default_host() - - -def default_host(): - """A default host, in place of anything better - - This may be considered as reference for the - interface a host must implement. It also ensures - that the system runs, even when nothing is there - to support it. - - """ - - host = types.ModuleType("defaultHost") - - def ls(): - return list() - - host.__dict__.update({ - "ls": ls - }) - - return host + _registered_host["_"] = None def debug_host(): From 636e46cfd673f13bba211024bf1b56180f17abad Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 15:29:18 +0200 Subject: [PATCH 077/124] implemented functions to query project and asset documents based on current context --- openpype/pipeline/context_tools.py | 52 ++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index fd4dc6e3fd..80ad939ccd 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -10,6 +10,11 @@ import pyblish.api from pyblish.lib import MessageHandler import openpype +from openpype.client import ( + get_project, + get_asset_by_id, + get_asset_by_name, +) from openpype.modules import load_modules, ModulesManager from openpype.settings import get_project_settings from openpype.lib import filter_pyblish_plugins @@ -282,3 +287,50 @@ def debug_host(): }) return host + + +def get_current_project(fields=None): + """Helper function to get project document based on global Session. + + This function should be called only in process where host is installed. + + Returns: + dict: Project document. + None: Project is not set. + """ + + project_name = legacy_io.active_project() + return get_project(project_name, fields=fields) + + +def get_current_project_asset(asset_name=None, asset_id=None, fields=None): + """Helper function to get asset document based on global Session. + + This function should be called only in process where host is installed. + + Asset is found out based on passed asset name or id (not both). Asset name + is not used for filtering if asset id is passed. When both asset name and + id are missing then asset name from current process is used. + + Args: + asset_name (str): Name of asset used for filter. + asset_id (Union[str, ObjectId]): Asset document id. If entered then + is used as only filter. + fields (Union[List[str], None]): Limit returned data of asset documents + to specific keys. + + Returns: + dict: Asset document. + None: Asset is not set or not exist. + """ + + project_name = legacy_io.active_project() + if asset_id: + return get_asset_by_id(project_name, asset_id, fields=fields) + + if not asset_name: + asset_name = legacy_io.Session.get("AVALON_ASSET") + # Skip if is not set even on context + if not asset_name: + return None + return get_asset_by_name(project_name, asset_name, fields=fields) From 35bd841939a78a5d963bb2972c4b14b0bace13b4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 15:31:51 +0200 Subject: [PATCH 078/124] marked 'get_asset' as deprecated --- openpype/lib/avalon_context.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 76ed6cbbd3..7ed22d6de6 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -236,7 +236,7 @@ def any_outdated(): return False -@with_pipeline_io +@deprecated("openpype.pipeline.context_tools.get_current_project_asset") def get_asset(asset_name=None): """ Returning asset document from database by its name. @@ -249,15 +249,9 @@ def get_asset(asset_name=None): (MongoDB document) """ - project_name = legacy_io.active_project() - if not asset_name: - asset_name = legacy_io.Session["AVALON_ASSET"] + from openpype.pipeline.context_tools import get_current_project_asset - asset_document = get_asset_by_name(project_name, asset_name) - if not asset_document: - raise TypeError("Entity \"{}\" was not found in DB".format(asset_name)) - - return asset_document + return get_current_project_asset(asset_name=asset_name) def get_system_general_anatomy_data(system_settings=None): From de0c0effe60e38f07bc47f577f8e5fb67f61814c Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Fri, 15 Jul 2022 15:39:54 +0200 Subject: [PATCH 079/124] reencode with concat, fix audio --- .../plugins/publish/extract_review_slate.py | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 28685c2e90..737b7db295 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -285,36 +285,32 @@ class ExtractReviewSlate(openpype.api.Extractor): audio_channels, audio_sample_rate, audio_channel_layout, + input_frame_rate ) # replace slate with silent slate for concat slate_v_path = slate_silent_path - # create ffmpeg concat text file path - conc_text_file = input_file.replace(ext, "") + "_concat" + ".txt" - conc_text_path = os.path.join( - os.path.normpath(stagingdir), conc_text_file) - _remove_at_end.append(conc_text_path) - self.log.debug("__ conc_text_path: {}".format(conc_text_path)) - - new_line = "\n" - with open(conc_text_path, "w") as conc_text_f: - conc_text_f.writelines([ - "file {}".format( - slate_v_path.replace("\\", "/")), - new_line, - "file {}".format(input_path.replace("\\", "/")) - ]) - - # concat slate and videos together + # concat slate and videos together with concat filter + # this will reencode the output + if input_audio: + fmap = [ + "[0:v] [0:a] [1:v] [1:a] concat=n=2:v=1:a=1 [v] [a]", + "-map", '[v]', + "-map", '[a]' + ] + else: + fmap = [ + "[0:v] [1:v] concat=n=2:v=1:a=0 [v]", + "-map", '[v]' + ] concat_args = [ ffmpeg_path, - "-y", - "-f", "concat", - "-safe", "0", - "-i", conc_text_path, - "-c", "copy", + "-i", slate_v_path, + "-i", input_path, + "-filter_complex", ] + concat_args.extend(fmap) if offset_timecode: concat_args.extend(["-timecode", offset_timecode]) # NOTE: Added because of OP Atom demuxers @@ -328,6 +324,10 @@ class ExtractReviewSlate(openpype.api.Extractor): copy_args = ( "-metadata", "-metadata:s:v:0", + "-codec:v", + "-pixfmt", + "-b:v", + "-b:a", ) args = source_ffmpeg_cmd.split(" ") for indx, arg in enumerate(args): @@ -335,12 +335,14 @@ class ExtractReviewSlate(openpype.api.Extractor): concat_args.append(arg) # assumes arg has one parameter concat_args.append(args[indx + 1]) + concat_args.append("-y") # add final output path concat_args.append(output_path) # ffmpeg concat subprocess self.log.debug( - "Executing concat: {}".format(" ".join(concat_args)) + "Executing concat filter: {}".format + (" ".join(concat_args)) ) openpype.api.run_subprocess( concat_args, logger=self.log @@ -488,9 +490,10 @@ class ExtractReviewSlate(openpype.api.Extractor): audio_channels, audio_sample_rate, audio_channel_layout, + input_frame_rate ): # Get duration of one frame in micro seconds - items = audio_sample_rate.split("/") + items = input_frame_rate.split("/") if len(items) == 1: one_frame_duration = 1.0 / float(items[0]) elif len(items) == 2: From ad8a7c86e4b655014e6dc776c813e9966cb9e1f3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 15:57:01 +0200 Subject: [PATCH 080/124] use 'get_current_project_asset' in hosts --- openpype/hosts/harmony/api/pipeline.py | 5 ++++- openpype/hosts/hiero/api/plugin.py | 3 ++- openpype/hosts/houdini/api/lib.py | 4 ++-- openpype/hosts/maya/api/lib.py | 14 ++++++++------ .../hosts/maya/plugins/create/create_render.py | 6 +++--- .../maya/plugins/publish/validate_maya_units.py | 10 +++++++--- openpype/hosts/nuke/api/lib.py | 4 ++-- .../hosts/nuke/plugins/publish/validate_script.py | 10 +++++----- openpype/hosts/resolve/api/plugin.py | 4 ++-- .../plugins/publish/collect_editorial.py | 3 ++- .../plugins/publish/collect_editorial_instances.py | 8 ++++++-- .../plugins/publish/validate_frame_ranges.py | 5 +++-- .../hosts/unreal/plugins/load/load_animation.py | 9 ++++++--- openpype/hosts/unreal/plugins/load/load_layout.py | 5 +++-- 14 files changed, 55 insertions(+), 35 deletions(-) diff --git a/openpype/hosts/harmony/api/pipeline.py b/openpype/hosts/harmony/api/pipeline.py index 86b5753f7e..94ca134205 100644 --- a/openpype/hosts/harmony/api/pipeline.py +++ b/openpype/hosts/harmony/api/pipeline.py @@ -15,6 +15,7 @@ from openpype.pipeline import ( deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) +from openpype.pipeline.context_tools import get_current_project_asset import openpype.hosts.harmony import openpype.hosts.harmony.api as harmony @@ -50,7 +51,9 @@ def get_asset_settings(): dict: Scene data. """ - asset_data = lib.get_asset()["data"] + + asset_doc = get_current_project_asset() + asset_data = asset_doc["data"] fps = asset_data.get("fps") frame_start = asset_data.get("frameStart") frame_end = asset_data.get("frameEnd") diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index add416d04e..28a9dfb492 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -10,6 +10,7 @@ import qargparse import openpype.api as openpype from openpype.pipeline import LoaderPlugin, LegacyCreator +from openpype.pipeline.context_tools import get_current_project_asset from . import lib log = openpype.Logger().get_logger(__name__) @@ -484,7 +485,7 @@ class ClipLoader: """ asset_name = self.context["representation"]["context"]["asset"] - asset_doc = openpype.get_asset(asset_name) + asset_doc = get_current_project_asset(asset_name) log.debug("__ asset_doc: {}".format(pformat(asset_doc))) self.data["assetData"] = asset_doc["data"] diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index dd8a5ba473..c8a7f92bb9 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -5,8 +5,8 @@ from contextlib import contextmanager import six from openpype.client import get_asset_by_name -from openpype.api import get_asset from openpype.pipeline import legacy_io +from openpype.pipeline.context_tools import get_current_project_asset import hou @@ -16,7 +16,7 @@ log = logging.getLogger(__name__) def get_asset_fps(): """Return current asset fps.""" - return get_asset()["data"].get("fps") + return get_current_project_asset()["data"].get("fps") def set_id(node, unique_id, overwrite=False): diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index e4221978c0..58e160cb2f 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -23,7 +23,6 @@ from openpype.client import ( get_last_versions, get_representation_by_name ) -from openpype import lib from openpype.api import get_anatomy_settings from openpype.pipeline import ( legacy_io, @@ -33,6 +32,7 @@ from openpype.pipeline import ( load_container, registered_host, ) +from openpype.pipeline.context_tools import get_current_project_asset from .commands import reset_frame_range @@ -2174,7 +2174,7 @@ def reset_scene_resolution(): project_name = legacy_io.active_project() project_doc = get_project(project_name) project_data = project_doc["data"] - asset_data = lib.get_asset()["data"] + asset_data = get_current_project_asset()["data"] # Set project resolution width_key = "resolutionWidth" @@ -2208,7 +2208,8 @@ def set_context_settings(): project_name = legacy_io.active_project() project_doc = get_project(project_name) project_data = project_doc["data"] - asset_data = lib.get_asset()["data"] + asset_doc = get_current_project_asset(fields=["data.fps"]) + asset_data = asset_doc.get("data", {}) # Set project fps fps = asset_data.get("fps", project_data.get("fps", 25)) @@ -2233,7 +2234,7 @@ def validate_fps(): """ - fps = lib.get_asset()["data"]["fps"] + fps = get_current_project_asset(fields=["data.fps"])["data"]["fps"] # TODO(antirotor): This is hack as for framerates having multiple # decimal places. FTrack is ceiling decimal values on # fps to two decimal places but Maya 2019+ is reporting those fps @@ -3051,8 +3052,9 @@ def update_content_on_context_change(): This will update scene content to match new asset on context change """ scene_sets = cmds.listSets(allSets=True) - new_asset = legacy_io.Session["AVALON_ASSET"] - new_data = lib.get_asset()["data"] + asset_doc = get_current_project_asset() + new_asset = asset_doc["name"] + new_data = asset_doc["data"] for s in scene_sets: try: if cmds.getAttr("{}.id".format(s)) == "pyblish.avalon.instance": diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 93ee6679e5..de07a0b23d 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -15,13 +15,13 @@ from openpype.hosts.maya.api import ( from openpype.lib import requests_get from openpype.api import ( get_system_settings, - get_project_settings, - get_asset) + get_project_settings) from openpype.modules import ModulesManager from openpype.pipeline import ( CreatorError, legacy_io, ) +from openpype.pipeline.context_tools import get_current_project_asset class CreateRender(plugin.Creator): @@ -413,7 +413,7 @@ class CreateRender(plugin.Creator): prefix, type="string") - asset = get_asset() + asset = get_current_project_asset() if renderer == "arnold": # set format to exr diff --git a/openpype/hosts/maya/plugins/publish/validate_maya_units.py b/openpype/hosts/maya/plugins/publish/validate_maya_units.py index d5a8c350d5..5f67adec76 100644 --- a/openpype/hosts/maya/plugins/publish/validate_maya_units.py +++ b/openpype/hosts/maya/plugins/publish/validate_maya_units.py @@ -2,8 +2,8 @@ import maya.cmds as cmds import pyblish.api import openpype.api -from openpype import lib import openpype.hosts.maya.api.lib as mayalib +from openpype.pipeline.context_tools import get_current_project_asset from math import ceil @@ -41,7 +41,9 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): # now flooring the value? fps = float_round(context.data.get('fps'), 2, ceil) - asset_fps = lib.get_asset()["data"]["fps"] + # TODO repace query with using 'context.data["assetEntity"]' + asset_doc = get_current_project_asset() + asset_fps = asset_doc["data"]["fps"] self.log.info('Units (linear): {0}'.format(linearunits)) self.log.info('Units (angular): {0}'.format(angularunits)) @@ -91,5 +93,7 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): cls.log.debug(current_linear) cls.log.info("Setting time unit to match project") - asset_fps = lib.get_asset()["data"]["fps"] + # TODO repace query with using 'context.data["assetEntity"]' + asset_doc = get_current_project_asset() + asset_fps = asset_doc["data"]["fps"] mayalib.set_scene_fps(asset_fps) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 0929415c00..7be7c1169c 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -24,7 +24,6 @@ from openpype.api import ( BuildWorkfile, get_version_from_path, get_workdir_data, - get_asset, get_current_project_settings, ) from openpype.tools.utils import host_tools @@ -40,6 +39,7 @@ from openpype.pipeline import ( legacy_io, Anatomy, ) +from openpype.pipeline.context_tools import get_current_project_asset from . import gizmo_menu @@ -1766,7 +1766,7 @@ class WorkfileSettings(object): kwargs.get("asset_name") or legacy_io.Session["AVALON_ASSET"] ) - self._asset_entity = get_asset(self._asset) + self._asset_entity = get_current_project_asset(self._asset) self._root_node = root_node or nuke.root() self._nodes = self.get_nodes(nodes=nodes) diff --git a/openpype/hosts/nuke/plugins/publish/validate_script.py b/openpype/hosts/nuke/plugins/publish/validate_script.py index 9bda0da85e..b8d7494b9d 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_script.py +++ b/openpype/hosts/nuke/plugins/publish/validate_script.py @@ -1,7 +1,6 @@ import pyblish.api -from openpype.client import get_project, get_asset_by_id -from openpype import lib +from openpype.client import get_project, get_asset_by_id, get_asset_by_name from openpype.pipeline import legacy_io @@ -17,10 +16,11 @@ class ValidateScript(pyblish.api.InstancePlugin): def process(self, instance): ctx_data = instance.context.data - asset_name = ctx_data["asset"] - asset = lib.get_asset(asset_name) - asset_data = asset["data"] project_name = legacy_io.active_project() + asset_name = ctx_data["asset"] + # TODO repace query with using 'instance.data["assetEntity"]' + asset = get_asset_by_name(project_name, asset_name) + asset_data = asset["data"] # These attributes will be checked attributes = [ diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 49b478fb3b..b03125d502 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -4,11 +4,11 @@ import uuid import qargparse from Qt import QtWidgets, QtCore -import openpype.api as pype from openpype.pipeline import ( LegacyCreator, LoaderPlugin, ) +from openpype.pipeline.context_tools import get_current_project_asset from openpype.hosts import resolve from . import lib @@ -375,7 +375,7 @@ class ClipLoader: """ asset_name = self.context["representation"]["context"]["asset"] - self.data["assetData"] = pype.get_asset(asset_name)["data"] + self.data["assetData"] = get_current_project_asset(asset_name)["data"] def load(self): # create project bin for the media to be imported into diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py index 0a1d29ccdc..8633d4bf9d 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py @@ -19,6 +19,7 @@ import os import opentimelineio as otio import pyblish.api from openpype import lib as plib +from openpype.pipeline.context_tools import get_current_project_asset class OTIO_View(pyblish.api.Action): @@ -116,7 +117,7 @@ class CollectEditorial(pyblish.api.InstancePlugin): if extension == ".edl": # EDL has no frame rate embedded so needs explicit # frame rate else 24 is asssumed. - kwargs["rate"] = plib.get_asset()["data"]["fps"] + kwargs["rate"] = get_current_project_asset()["data"]["fps"] instance.data["otio_timeline"] = otio.adapters.read_from_file( file_path, **kwargs) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py index d0d36bb717..3237fbbe12 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py @@ -1,8 +1,12 @@ import os +from copy import deepcopy + import opentimelineio as otio import pyblish.api + from openpype import lib as plib -from copy import deepcopy +from openpype.pipeline.context_tools import get_current_project_asset + class CollectInstances(pyblish.api.InstancePlugin): """Collect instances from editorial's OTIO sequence""" @@ -48,7 +52,7 @@ class CollectInstances(pyblish.api.InstancePlugin): # get timeline otio data timeline = instance.data["otio_timeline"] - fps = plib.get_asset()["data"]["fps"] + fps = get_current_project_asset()["data"]["fps"] tracks = timeline.each_child( descended_from_type=otio.schema.Track diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py index 005157af62..ff7f60354e 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py @@ -3,8 +3,8 @@ import re import pyblish.api import openpype.api -from openpype import lib from openpype.pipeline import PublishXmlValidationError +from openpype.pipeline.context_tools import get_current_project_asset class ValidateFrameRange(pyblish.api.InstancePlugin): @@ -27,7 +27,8 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): for pattern in self.skip_timelines_check): self.log.info("Skipping for {} task".format(instance.data["task"])) - asset_data = lib.get_asset(instance.data["asset"])["data"] + # TODO repace query with using 'instance.data["assetEntity"]' + asset_data = get_current_project_asset(instance.data["asset"])["data"] frame_start = asset_data["frameStart"] frame_end = asset_data["frameEnd"] handle_start = asset_data["handleStart"] diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index da2830bc52..1fe0bef462 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -8,13 +8,13 @@ from unreal import EditorAssetLibrary from unreal import MovieSceneSkeletalAnimationTrack from unreal import MovieSceneSkeletalAnimationSection +from openpype.pipeline.context_tools import get_current_project_asset from openpype.pipeline import ( get_representation_path, AVALON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline -from openpype.api import get_asset class AnimationFBXLoader(plugin.Loader): @@ -53,6 +53,8 @@ class AnimationFBXLoader(plugin.Loader): if not actor: return None + asset_doc = get_current_project_asset(fields=["data.fps"]) + task.set_editor_property('filename', self.fname) task.set_editor_property('destination_path', asset_dir) task.set_editor_property('destination_name', asset_name) @@ -80,7 +82,7 @@ class AnimationFBXLoader(plugin.Loader): task.options.anim_sequence_import_data.set_editor_property( 'use_default_sample_rate', False) task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', get_asset()["data"].get("fps")) + 'custom_sample_rate', asset_doc.get("data", {}).get("fps")) task.options.anim_sequence_import_data.set_editor_property( 'import_custom_attribute', True) task.options.anim_sequence_import_data.set_editor_property( @@ -246,6 +248,7 @@ class AnimationFBXLoader(plugin.Loader): def update(self, container, representation): name = container["asset_name"] source_path = get_representation_path(representation) + asset_doc = get_current_project_asset(fields=["data.fps"]) destination_path = container["namespace"] task = unreal.AssetImportTask() @@ -279,7 +282,7 @@ class AnimationFBXLoader(plugin.Loader): task.options.anim_sequence_import_data.set_editor_property( 'use_default_sample_rate', False) task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', get_asset()["data"].get("fps")) + 'custom_sample_rate', asset_doc.get("data", {}).get("fps")) task.options.anim_sequence_import_data.set_editor_property( 'import_custom_attribute', True) task.options.anim_sequence_import_data.set_editor_property( diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 3f16a68ead..01d589c69b 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -20,7 +20,7 @@ from openpype.pipeline import ( AVALON_CONTAINER_ID, legacy_io, ) -from openpype.api import get_asset +from openpype.pipeline.context_tools import get_current_project_asset from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -225,6 +225,7 @@ class LayoutLoader(plugin.Loader): anim_path = f"{asset_dir}/animations/{anim_file_name}" + asset_doc = get_current_project_asset() # Import animation task = unreal.AssetImportTask() task.options = unreal.FbxImportUI() @@ -259,7 +260,7 @@ class LayoutLoader(plugin.Loader): task.options.anim_sequence_import_data.set_editor_property( 'use_default_sample_rate', False) task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', get_asset()["data"].get("fps")) + 'custom_sample_rate', asset_doc.get("data", {}).get("fps")) task.options.anim_sequence_import_data.set_editor_property( 'import_custom_attribute', True) task.options.anim_sequence_import_data.set_editor_property( From b5d7ae0d2a38d93ba5014c9a1aec455b9ca982ce Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Fri, 15 Jul 2022 16:29:05 +0200 Subject: [PATCH 081/124] no need to copy codec and pixel format --- openpype/plugins/publish/extract_review_slate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 737b7db295..2edaf10e6b 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -324,8 +324,6 @@ class ExtractReviewSlate(openpype.api.Extractor): copy_args = ( "-metadata", "-metadata:s:v:0", - "-codec:v", - "-pixfmt", "-b:v", "-b:a", ) From e8b4a3389e9ac0095bdafcdd008398dc69aac38c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 16:33:47 +0200 Subject: [PATCH 082/124] added comment do harmony plugin --- .../hosts/harmony/plugins/publish/validate_scene_settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/harmony/plugins/publish/validate_scene_settings.py b/openpype/hosts/harmony/plugins/publish/validate_scene_settings.py index 4c3a6c4465..936533abd6 100644 --- a/openpype/hosts/harmony/plugins/publish/validate_scene_settings.py +++ b/openpype/hosts/harmony/plugins/publish/validate_scene_settings.py @@ -55,6 +55,10 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): def process(self, instance): """Plugin entry point.""" + + # TODO 'get_asset_settings' could expect asset document as argument + # which is available on 'context.data["assetEntity"]' + # - the same approach can be used in 'ValidateSceneSettingsRepair' expected_settings = harmony.get_asset_settings() self.log.info("scene settings from DB:".format(expected_settings)) From 6f521242cbaa88a4bae403fa7b23c4d9faa9cd18 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 17:01:00 +0200 Subject: [PATCH 083/124] implemented functions to filter containers into 4 possible categories --- openpype/pipeline/load/__init__.py | 4 + openpype/pipeline/load/utils.py | 132 +++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index 6e7612d4c1..e05dde2f9c 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -24,6 +24,8 @@ from .utils import ( loaders_from_repre_context, loaders_from_representation, + + filter_containers, ) from .plugins import ( @@ -66,6 +68,8 @@ __all__ = ( "loaders_from_repre_context", "loaders_from_representation", + "filter_containers", + # plugins.py "LoaderPlugin", "SubsetLoaderPlugin", diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 2c213aff6f..68850c095a 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -4,6 +4,7 @@ import copy import getpass import logging import inspect +import collections import numbers from openpype.client import ( @@ -15,6 +16,7 @@ from openpype.client import ( get_last_version_by_subset_id, get_hero_version_by_subset_id, get_version_by_name, + get_last_versions, get_representations, get_representation_by_id, get_representation_by_name, @@ -28,6 +30,11 @@ from openpype.pipeline import ( log = logging.getLogger(__name__) +ContainersFilterResult = collections.namedtuple( + "ContainersFilterResult", + ["latest", "outdated", "not_foud", "invalid"] +) + class HeroVersionType(object): def __init__(self, version): @@ -685,3 +692,128 @@ def loaders_from_representation(loaders, representation): context = get_representation_context(representation) return loaders_from_repre_context(loaders, context) + + +def filter_containers(containers, project_name): + """Filter containers and split them into 4 categories. + + Categories are 'latest', 'outdated', 'invalid' and 'not_found'. + The 'lastest' containers are from last version, 'outdated' are not, + 'invalid' are invalid containers (invalid content) and 'not_foud' has + some missing entity in database. + + Args: + containers (list[dict]): List of containers referenced into scene. + project_name (str): Name of project in which context shoud look for + versions. + + Returns: + ContainersFilterResult: Named tuple with 'latest', 'outdated', + 'invalid' and 'not_found' containers. + """ + + outdated_containers = [] + uptodate_containers = [] + not_found_containers = [] + invalid_containers = [] + output = ContainersFilterResult( + uptodate_containers, + outdated_containers, + not_found_containers, + invalid_containers + ) + # Query representation docs to get it's version ids + repre_ids = { + container["representation"] + for container in containers + if container["representation"] + } + if not repre_ids: + if containers: + invalid_containers.extend(containers) + return output + + repre_docs = get_representations( + project_name, + representation_ids=repre_ids, + fields=["_id", "parent"] + ) + # Store representations by stringified representation id + repre_docs_by_str_id = {} + repre_docs_by_version_id = collections.defaultdict(list) + for repre_doc in repre_docs: + repre_id = str(repre_doc["_id"]) + version_id = repre_doc["parent"] + repre_docs_by_str_id[repre_id] = repre_doc + repre_docs_by_version_id[version_id].append(repre_doc) + + # Query version docs to get it's subset ids + # - also query hero version to be able identify if representation + # belongs to existing version + version_docs = get_versions( + project_name, + version_ids=repre_docs_by_version_id.keys(), + hero=True, + fields=["_id", "parent", "type"] + ) + verisons_by_id = {} + versions_by_subset_id = collections.defaultdict(list) + hero_version_ids = set() + for version_doc in version_docs: + version_id = version_doc["_id"] + # Store versions by their ids + verisons_by_id[version_id] = version_doc + # There's no need to query subsets for hero versions + # - they are considered as latest? + if version_doc["type"] == "hero_version": + hero_version_ids.add(version_id) + continue + subset_id = version_doc["parent"] + versions_by_subset_id[subset_id].append(version_doc) + + last_versions = get_last_versions( + project_name, + subset_ids=versions_by_subset_id.keys(), + fields=["_id"] + ) + # Figure out which versions are outdated + outdated_version_ids = set() + for subset_id, last_version_doc in last_versions.items(): + for version_doc in versions_by_subset_id[subset_id]: + version_id = version_doc["_id"] + if version_id != last_version_doc["_id"]: + outdated_version_ids.add(version_id) + + # Based on all collected data figure out which containers are outdated + # - log out if there are missing representation or version documents + for container in containers: + container_name = container["objectName"] + repre_id = container["representation"] + if not repre_id: + invalid_containers.append(container) + continue + + repre_doc = repre_docs_by_str_id.get(repre_id) + if not repre_doc: + log.debug(( + "Container '{}' has an invalid representation." + " It is missing in the database." + ).format(container_name)) + not_found_containers.append(container) + continue + + version_id = repre_doc["parent"] + if version_id in outdated_version_ids: + outdated_containers.append(container) + + elif version_id not in verisons_by_id: + log.debug(( + "Representation on container '{}' has an invalid version." + " It is missing in the database." + ).format(container_name)) + not_found_containers.append(container) + + else: + uptodate_containers.append(container) + + return output From 1ec708ce7f5786a6cff9bbd490beff0872553d01 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 17:01:28 +0200 Subject: [PATCH 084/124] added helper functions to get outdated containers or just check if there are any outdated --- openpype/pipeline/load/__init__.py | 4 ++++ openpype/pipeline/load/utils.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index e05dde2f9c..e46d9f152b 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -25,6 +25,8 @@ from .utils import ( loaders_from_repre_context, loaders_from_representation, + any_outdated_containers, + get_outdated_containers, filter_containers, ) @@ -68,6 +70,8 @@ __all__ = ( "loaders_from_repre_context", "loaders_from_representation", + "any_outdated_containers", + "get_outdated_containers", "filter_containers", # plugins.py diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 68850c095a..a9aa240ff6 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -694,6 +694,35 @@ def loaders_from_representation(loaders, representation): return loaders_from_repre_context(loaders, context) +def any_outdated_containers(host=None, project_name=None): + """Check if there are any outdated containers in scene.""" + + if get_outdated_containers(host, project_name): + return True + return False + + +def get_outdated_containers(host=None, project_name=None): + """Collect outdated containers from host scene. + + Currently registered host and project in global session are used if + arguments are not passed. + + Args: + host (ModuleType): Host implementation with 'ls' function available. + project_name (str): Name of project in which context we are. + """ + + if host is None: + from openpype.pipeline import registered_host + host = registered_host() + + if project_name is None: + project_name = legacy_io.active_project() + containers = host.ls() + return filter_containers(containers, project_name).outdated + + def filter_containers(containers, project_name): """Filter containers and split them into 4 categories. From f3b628843b5f8e986d4d52483bbaf9a94a0440b4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 17:06:35 +0200 Subject: [PATCH 085/124] marked 'any_outdated' in 'openpype.lib' as deprecated --- openpype/lib/avalon_context.py | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 76ed6cbbd3..b3113ce188 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -19,7 +19,6 @@ from openpype.client import ( get_last_versions, get_last_version_by_subset_id, get_representations, - get_representation_by_id, get_workfile_info, ) from openpype.settings import ( @@ -208,32 +207,13 @@ def is_latest(representation): return version["_id"] == last_version["_id"] -@with_pipeline_io +@deprecated("openpype.pipeline.load.any_outdated_containers") def any_outdated(): """Return whether the current scene has any outdated content""" - from openpype.pipeline import registered_host - project_name = legacy_io.active_project() - checked = set() - host = registered_host() - for container in host.ls(): - representation = container['representation'] - if representation in checked: - continue + from openpype.pipeline.load import any_outdated_containers - representation_doc = get_representation_by_id( - project_name, representation, fields=["parent"] - ) - if representation_doc and not is_latest(representation_doc): - return True - elif not representation_doc: - log.debug("Container '{objectName}' has an invalid " - "representation, it is missing in the " - "database".format(**container)) - - checked.add(representation) - - return False + return any_outdated_containers() @with_pipeline_io From 6e90984528199eb697d55ba2c8fe0df8d7cec87b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 17:07:22 +0200 Subject: [PATCH 086/124] replace usage of 'any_outdated' with 'any_outdated_containers' --- openpype/hosts/aftereffects/api/pipeline.py | 4 ++-- openpype/hosts/houdini/api/pipeline.py | 4 ++-- openpype/hosts/maya/api/pipeline.py | 4 ++-- openpype/hosts/photoshop/api/pipeline.py | 5 ++--- openpype/plugins/publish/validate_containers.py | 4 ++-- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 0bc47665b0..c13c22ced5 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -1,5 +1,4 @@ import os -import sys from Qt import QtWidgets @@ -15,6 +14,7 @@ from openpype.pipeline import ( AVALON_CONTAINER_ID, legacy_io, ) +from openpype.pipeline.load import any_outdated_containers import openpype.hosts.aftereffects from openpype.lib import register_event_callback @@ -136,7 +136,7 @@ def ls(): def check_inventory(): """Checks loaded containers if they are of highest version""" - if not lib.any_outdated(): + if not any_outdated_containers(): return # Warn about outdated containers. diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 7048accceb..b5f5459392 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -12,13 +12,13 @@ from openpype.pipeline import ( register_loader_plugin_path, AVALON_CONTAINER_ID, ) +from openpype.pipeline.load import any_outdated_containers import openpype.hosts.houdini from openpype.hosts.houdini.api import lib from openpype.lib import ( register_event_callback, emit_event, - any_outdated, ) from .lib import get_asset_fps @@ -245,7 +245,7 @@ def on_open(): # ensure it is using correct FPS for the asset lib.validate_fps() - if any_outdated(): + if any_outdated_containers(): from openpype.widgets import popup log.warning("Scene has outdated content.") diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index d08e8d1926..f565f6a308 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -13,7 +13,6 @@ from openpype.host import HostBase, IWorkfileHost, ILoadHost import openpype.hosts.maya from openpype.tools.utils import host_tools from openpype.lib import ( - any_outdated, register_event_callback, emit_event ) @@ -28,6 +27,7 @@ from openpype.pipeline import ( deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) +from openpype.pipeline.load import any_outdated_containers from openpype.hosts.maya.lib import copy_workspace_mel from . import menu, lib from .workio import ( @@ -470,7 +470,7 @@ def on_open(): lib.validate_fps() lib.fix_incompatible_containers() - if any_outdated(): + if any_outdated_containers(): log.warning("Scene has outdated content.") # Find maya main window diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 20a6e3169f..ee150d1808 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -1,6 +1,5 @@ import os from Qt import QtWidgets -from bson.objectid import ObjectId import pyblish.api @@ -13,8 +12,8 @@ from openpype.pipeline import ( deregister_loader_plugin_path, deregister_creator_plugin_path, AVALON_CONTAINER_ID, - registered_host, ) +from openpype.pipeline.load import any_outdated_containers import openpype.hosts.photoshop from . import lib @@ -30,7 +29,7 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") def check_inventory(): - if not lib.any_outdated(): + if not any_outdated_containers(): return # Warn about outdated containers. diff --git a/openpype/plugins/publish/validate_containers.py b/openpype/plugins/publish/validate_containers.py index ce91bd3396..7732ec5ea9 100644 --- a/openpype/plugins/publish/validate_containers.py +++ b/openpype/plugins/publish/validate_containers.py @@ -1,5 +1,5 @@ import pyblish.api -import openpype.lib +from openpype.pipeline.load import any_outdated_containers class ShowInventory(pyblish.api.Action): @@ -24,5 +24,5 @@ class ValidateContainers(pyblish.api.ContextPlugin): actions = [ShowInventory] def process(self, context): - if openpype.lib.any_outdated(): + if any_outdated_containers(): raise ValueError("There are outdated containers in the scene.") From b0ce3e851ddc03850f1c05bc3a7eda78a7621708 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 17:25:53 +0200 Subject: [PATCH 087/124] added function to check if version is latest --- openpype/client/__init__.py | 4 ++++ openpype/client/entities.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/openpype/client/__init__.py b/openpype/client/__init__.py index 97e6755d09..4b8213a8ac 100644 --- a/openpype/client/__init__.py +++ b/openpype/client/__init__.py @@ -25,6 +25,8 @@ from .entities import ( get_last_version_by_subset_name, get_output_link_versions, + version_is_latest, + get_representation_by_id, get_representation_by_name, get_representations, @@ -66,6 +68,8 @@ __all__ = ( "get_last_version_by_subset_name", "get_output_link_versions", + "version_is_latest", + "get_representation_by_id", "get_representation_by_name", "get_representations", diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 9d65355d1b..468f569c7f 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -557,6 +557,42 @@ def get_version_by_name(project_name, version, subset_id, fields=None): return conn.find_one(query_filter, _prepare_fields(fields)) +def version_is_latest(project_name, version_id): + """Is version the latest from it's subset. + + Note: + Hero versions are considered as latest. + + Todo: + Maybe raise exception when version was not found? + + Args: + project_name (str):Name of project where to look for queried entities. + version_id (str|ObjectId): Version id which is checked. + + Returns: + bool: True if is latest version from subset else False. + """ + + version_id = _convert_id(version_id) + if not version_id: + return False + version_doc = get_version_by_id( + project_name, version_id, fields=["_id", "type", "parent"] + ) + # What to de when version is not found? + if not version_doc: + return False + + if version_doc["type"] == "hero_version": + return True + + last_version = get_last_version_by_subset_id( + project_name, version_doc["parent"], fields=["_id"] + ) + return last_version["_id"] == version_id + + def _get_versions( project_name, subset_ids=None, From 95eb83d8e05749a430d04c22a6b0486b983ba315 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 17:45:45 +0200 Subject: [PATCH 088/124] use 'get_outdated_containers' in harmony --- openpype/hosts/harmony/api/pipeline.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/harmony/api/pipeline.py b/openpype/hosts/harmony/api/pipeline.py index 86b5753f7e..3246f1add9 100644 --- a/openpype/hosts/harmony/api/pipeline.py +++ b/openpype/hosts/harmony/api/pipeline.py @@ -5,16 +5,15 @@ import logging import pyblish.api from openpype import lib -from openpype.client import get_representation_by_id from openpype.lib import register_event_callback from openpype.pipeline import ( - legacy_io, register_loader_plugin_path, register_creator_plugin_path, deregister_loader_plugin_path, deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) +from openpype.pipeline.load import get_outdated_containers import openpype.hosts.harmony import openpype.hosts.harmony.api as harmony @@ -105,16 +104,7 @@ def check_inventory(): in Harmony. """ - project_name = legacy_io.active_project() - outdated_containers = [] - for container in ls(): - representation_id = container['representation'] - representation_doc = get_representation_by_id( - project_name, representation_id, fields=["parent"] - ) - if representation_doc and not lib.is_latest(representation_doc): - outdated_containers.append(container) - + outdated_containers = get_outdated_containers() if not outdated_containers: return From c8d18dafa1a9366ddf07f1451e8b926533fdf07a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 17:46:21 +0200 Subject: [PATCH 089/124] 'is_latest' moved to pipeline as 'is_representation_from_latest' --- .../harmony/plugins/load/load_background.py | 9 +++------ .../plugins/load/load_imagesequence.py | 4 ++-- .../harmony/plugins/load/load_template.py | 4 ++-- openpype/lib/avalon_context.py | 19 +++---------------- openpype/pipeline/context_tools.py | 15 +++++++++++++++ 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/harmony/plugins/load/load_background.py b/openpype/hosts/harmony/plugins/load/load_background.py index 9c01fe3cd8..9e9fcbfa32 100644 --- a/openpype/hosts/harmony/plugins/load/load_background.py +++ b/openpype/hosts/harmony/plugins/load/load_background.py @@ -5,8 +5,8 @@ from openpype.pipeline import ( load, get_representation_path, ) +from openpype.pipeline.context_tools import is_representation_from_latest import openpype.hosts.harmony.api as harmony -import openpype.lib copy_files = """function copyFile(srcFilename, dstFilename) @@ -280,9 +280,7 @@ class BackgroundLoader(load.LoaderPlugin): ) def update(self, container, representation): - path = get_representation_path(representation) - with open(path) as json_file: data = json.load(json_file) @@ -300,10 +298,9 @@ class BackgroundLoader(load.LoaderPlugin): bg_folder = os.path.dirname(path) - path = get_representation_path(representation) - print(container) + is_latest = is_representation_from_latest(representation["parent"]) for layer in sorted(layers): file_to_import = [ os.path.join(bg_folder, layer).replace("\\", "/") @@ -347,7 +344,7 @@ class BackgroundLoader(load.LoaderPlugin): } %s """ % (sig, sig) - if openpype.lib.is_latest(representation): + if is_latest: harmony.send({"function": func, "args": [node, "green"]}) else: harmony.send({"function": func, "args": [node, "red"]}) diff --git a/openpype/hosts/harmony/plugins/load/load_imagesequence.py b/openpype/hosts/harmony/plugins/load/load_imagesequence.py index 18695438d5..8d6421a6aa 100644 --- a/openpype/hosts/harmony/plugins/load/load_imagesequence.py +++ b/openpype/hosts/harmony/plugins/load/load_imagesequence.py @@ -10,8 +10,8 @@ from openpype.pipeline import ( load, get_representation_path, ) +from openpype.pipeline.context_tools import is_representation_from_latest import openpype.hosts.harmony.api as harmony -import openpype.lib class ImageSequenceLoader(load.LoaderPlugin): @@ -109,7 +109,7 @@ class ImageSequenceLoader(load.LoaderPlugin): ) # Colour node. - if openpype.lib.is_latest(representation): + if is_representation_from_latest(representation["parent"]): harmony.send( { "function": "PypeHarmony.setColor", diff --git a/openpype/hosts/harmony/plugins/load/load_template.py b/openpype/hosts/harmony/plugins/load/load_template.py index c6dc9d913b..8ddd3934f7 100644 --- a/openpype/hosts/harmony/plugins/load/load_template.py +++ b/openpype/hosts/harmony/plugins/load/load_template.py @@ -10,8 +10,8 @@ from openpype.pipeline import ( load, get_representation_path, ) +from openpype.pipeline.context_tools import is_representation_from_latest import openpype.hosts.harmony.api as harmony -import openpype.lib class TemplateLoader(load.LoaderPlugin): @@ -83,7 +83,7 @@ class TemplateLoader(load.LoaderPlugin): self_name = self.__class__.__name__ update_and_replace = False - if openpype.lib.is_latest(representation): + if is_representation_from_latest(representation["parent"]): self._set_green(node) else: self._set_red(node) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index b3113ce188..1108791953 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -15,7 +15,6 @@ from openpype.client import ( get_asset_by_name, get_subset_by_name, get_subsets, - get_version_by_id, get_last_versions, get_last_version_by_subset_id, get_representations, @@ -179,7 +178,7 @@ def with_pipeline_io(func): return wrapped -@with_pipeline_io +@deprecated("openpype.pipeline.context_tools.is_representation_from_latest") def is_latest(representation): """Return whether the representation is from latest version @@ -190,21 +189,9 @@ def is_latest(representation): bool: Whether the representation is of latest version. """ - project_name = legacy_io.active_project() - version = get_version_by_id( - project_name, - representation["parent"], - fields=["_id", "type", "parent"] - ) - if version["type"] == "hero_version": - return True + from openpype.pipeline.context_tools import is_representation_from_latest - # Get highest version under the parent - last_version = get_last_version_by_subset_id( - project_name, version["parent"], fields=["_id"] - ) - - return version["_id"] == last_version["_id"] + return is_representation_from_latest(representation) @deprecated("openpype.pipeline.load.any_outdated_containers") diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index e719e46514..e2f9df5dae 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -10,6 +10,7 @@ import pyblish.api from pyblish.lib import MessageHandler import openpype +from openpype.client import version_is_latest from openpype.modules import load_modules, ModulesManager from openpype.settings import get_project_settings from openpype.lib import filter_pyblish_plugins @@ -304,3 +305,17 @@ def debug_host(): }) return host + + +def is_representation_from_latest(representation): + """Return whether the representation is from latest version + + Args: + representation (dict): The representation document from the database. + + Returns: + bool: Whether the representation is of latest version. + """ + + project_name = legacy_io.active_project() + return version_is_latest(project_name, representation["parent"]) From 3aa38ae0cc7e0799c6b510ad258c8fe7e3315bfe Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 18:11:28 +0200 Subject: [PATCH 090/124] use 'get_last_version_by_subset_name' instead of 'get_latest_version' --- .../plugins/publish/submit_publish_job.py | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 9dd1428a63..9ef80efa50 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -10,7 +10,7 @@ import clique import pyblish.api -import openpype.api +from openpype.client import get_last_version_by_subset_name from openpype.pipeline import ( get_representation_path, legacy_io, @@ -333,8 +333,13 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # get latest version of subset # this will stop if subset wasn't published yet - version = openpype.api.get_latest_version(instance.data.get("asset"), - instance.data.get("subset")) + project_name = legacy_io.active_project() + version = get_last_version_by_subset_name( + project_name, + instance.data.get("subset"), + asset_name=instance.data.get("asset") + ) + # get its files based on extension subset_resources = get_resources(version, representation.get("ext")) r_col, _ = clique.assemble(subset_resources) @@ -1013,9 +1018,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): prev_start = None prev_end = None - version = openpype.api.get_latest_version(asset_name=asset, - subset_name=subset - ) + project_name = legacy_io.active_project() + version = get_last_version_by_subset_name( + project_name, + subset, + asset_name=asset + ) # Set prev start / end frames for comparison if not prev_start and not prev_end: @@ -1060,7 +1068,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): based on 'publish' template """ if not version: - version = openpype.api.get_latest_version(asset, subset) + project_name = legacy_io.active_project() + version = get_last_version_by_subset_name( + project_name, + subset, + asset_name=asset + ) if version: version = int(version["name"]) + 1 else: From 1a61bd03e027053f39196314f7866e59e004a4e8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 18:12:39 +0200 Subject: [PATCH 091/124] marked 'get_latest_version' as deprecated --- openpype/lib/avalon_context.py | 37 +++---------------- .../tests/test_lib_restructuralization.py | 1 - 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 1108791953..be5f1117a7 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -17,6 +17,7 @@ from openpype.client import ( get_subsets, get_last_versions, get_last_version_by_subset_id, + get_last_version_by_subset_name, get_representations, get_workfile_info, ) @@ -286,7 +287,7 @@ def get_linked_assets(asset_doc): return list(get_assets(project_name, link_ids)) -@with_pipeline_io +@deprecated("openpype.client.get_last_version_by_subset_name") def get_latest_version(asset_name, subset_name, dbcon=None, project_name=None): """Retrieve latest version from `asset_name`, and `subset_name`. @@ -307,6 +308,8 @@ def get_latest_version(asset_name, subset_name, dbcon=None, project_name=None): if not project_name: if not dbcon: + from openpype.pipeline import legacy_io + log.debug("Using `legacy_io` for query.") dbcon = legacy_io # Make sure is installed @@ -314,37 +317,9 @@ def get_latest_version(asset_name, subset_name, dbcon=None, project_name=None): project_name = dbcon.active_project() - log.debug(( - "Getting latest version for Project: \"{}\" Asset: \"{}\"" - " and Subset: \"{}\"" - ).format(project_name, asset_name, subset_name)) - - # Query asset document id by asset name - asset_doc = get_asset_by_name(project_name, asset_name, fields=["_id"]) - if not asset_doc: - log.info( - "Asset \"{}\" was not found in Database.".format(asset_name) - ) - return None - - subset_doc = get_subset_by_name( - project_name, subset_name, asset_doc["_id"] + return get_last_version_by_subset_name( + project_name, subset_name, asset_name=asset_name ) - if not subset_doc: - log.info( - "Subset \"{}\" was not found in Database.".format(subset_name) - ) - return None - - version_doc = get_last_version_by_subset_id( - project_name, subset_doc["_id"] - ) - if not version_doc: - log.info( - "Subset \"{}\" does not have any version yet.".format(subset_name) - ) - return None - return version_doc def get_workfile_template_key_from_context( diff --git a/openpype/tests/test_lib_restructuralization.py b/openpype/tests/test_lib_restructuralization.py index ccccc76a08..c8952e5a1c 100644 --- a/openpype/tests/test_lib_restructuralization.py +++ b/openpype/tests/test_lib_restructuralization.py @@ -22,7 +22,6 @@ def test_backward_compatibility(printer): from openpype.lib import any_outdated from openpype.lib import get_asset from openpype.lib import get_linked_assets - from openpype.lib import get_latest_version from openpype.lib import get_ffprobe_streams from openpype.hosts.fusion.lib import switch_item From 539d4c8fa99c26d4e7b1b226dd146b60d04b1622 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Jul 2022 18:55:20 +0200 Subject: [PATCH 092/124] modify docstring --- openpype/client/entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 468f569c7f..cc22a0a835 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -568,7 +568,7 @@ def version_is_latest(project_name, version_id): Args: project_name (str):Name of project where to look for queried entities. - version_id (str|ObjectId): Version id which is checked. + version_id (Union[str, ObjectId]): Version id which is checked. Returns: bool: True if is latest version from subset else False. From fd8a801f40050995d49e3cd8d885ec2cb6951152 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 10:03:34 +0200 Subject: [PATCH 093/124] fix typo --- openpype/client/entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index cc22a0a835..81640f75e5 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -580,7 +580,7 @@ def version_is_latest(project_name, version_id): version_doc = get_version_by_id( project_name, version_id, fields=["_id", "type", "parent"] ) - # What to de when version is not found? + # What to do when version is not found? if not version_doc: return False From 9ef9c79e8fb7cf6e2a783abe3eba1ba13f1eaa6d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 18 Jul 2022 11:34:30 +0200 Subject: [PATCH 094/124] move all hosts and families to the new integrator --- .../defaults/project_settings/global.json | 83 ++++--------------- .../schemas/schema_global_publish.json | 4 +- 2 files changed, 18 insertions(+), 69 deletions(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 545c792d47..d923fc65c9 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -172,74 +172,8 @@ }, "IntegrateAssetNew": { "hosts": [ - "aftereffects", - "blender", - "celaction", - "flame", - "fusion", - "harmony", - "hiero", - "houdini", - "nuke", - "photoshop", - "resolve", - "tvpaint", - "unreal", - "standalonepublisher", - "webpublisher" ], "families": [ - "workfile", - "pointcache", - "camera", - "animation", - "model", - "mayaAscii", - "mayaScene", - "setdress", - "layout", - "ass", - "vdbcache", - "scene", - "vrayproxy", - "vrayscene_layer", - "render", - "prerender", - "imagesequence", - "review", - "rendersetup", - "rig", - "plate", - "look", - "audio", - "yetiRig", - "yeticache", - "nukenodes", - "gizmo", - "source", - "matchmove", - "image", - "assembly", - "fbx", - "textures", - "action", - "harmony.template", - "harmony.palette", - "editorial", - "background", - "camerarig", - "redshiftproxy", - "effect", - "xgen", - "hda", - "usd", - "staticMesh", - "skeletalMesh", - "mvLook", - "mvUsd", - "mvUsdComposition", - "mvUsdOverride", - "simpleUnrealTexture" ], "template_name_profiles": [ { @@ -287,7 +221,22 @@ }, "IntegrateAsset": { "hosts": [ - "maya" + "maya", + "aftereffects", + "blender", + "celaction", + "flame", + "fusion", + "harmony", + "hiero", + "houdini", + "nuke", + "photoshop", + "resolve", + "tvpaint", + "unreal", + "standalonepublisher", + "webpublisher" ], "families": [ "workfile", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 71eed2e2de..5e3978a2df 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -584,7 +584,7 @@ "type": "dict", "collapsible": true, "key": "IntegrateAssetNew", - "label": "IntegrateAssetNew", + "label": "IntegrateAsset (Legacy)", "is_group": true, "children": [ { @@ -651,7 +651,7 @@ "type": "dict", "collapsible": true, "key": "IntegrateAsset", - "label": "IntegrateAsset", + "label": "Integrate Asset", "is_group": true, "children": [ { From 7646c54da87fe04570ba67027bbf5af308cc7b83 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 18 Jul 2022 11:36:09 +0200 Subject: [PATCH 095/124] move subset group collecting to early integrator --- ...{collect_subset_group.py => integrate_subset_group.py} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename openpype/plugins/publish/{collect_subset_group.py => integrate_subset_group.py} (94%) diff --git a/openpype/plugins/publish/collect_subset_group.py b/openpype/plugins/publish/integrate_subset_group.py similarity index 94% rename from openpype/plugins/publish/collect_subset_group.py rename to openpype/plugins/publish/integrate_subset_group.py index 56cd7de94e..4b566e8908 100644 --- a/openpype/plugins/publish/collect_subset_group.py +++ b/openpype/plugins/publish/integrate_subset_group.py @@ -17,12 +17,12 @@ from openpype.lib import ( ) -class CollectSubsetGroup(pyblish.api.InstancePlugin): - """Collect Subset Group for publish.""" +class IntegrateSubsetGroup(pyblish.api.InstancePlugin): + """Integrate Subset Group for publish.""" # Run after CollectAnatomyInstanceData - order = pyblish.api.CollectorOrder + 0.495 - label = "Collect Subset Group" + order = pyblish.api.IntegratorOrder - 0.1 + label = "Subset Group" # Attributes set by settings subset_grouping_profiles = None From 0c59f1539872981ff896119ef7bb729c4c08064d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 18 Jul 2022 11:38:28 +0200 Subject: [PATCH 096/124] rename old integrator to integrate legacy --- .../plugins/publish/{integrate_new.py => integrate_legacy.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/plugins/publish/{integrate_new.py => integrate_legacy.py} (100%) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_legacy.py similarity index 100% rename from openpype/plugins/publish/integrate_new.py rename to openpype/plugins/publish/integrate_legacy.py From 84781c12e570b085f533421c5c8ef0712f611504 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 18:12:04 +0200 Subject: [PATCH 097/124] call 'bulk_write' directly on legacy_io --- openpype/plugins/publish/integrate.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 5e86eb014a..790f96d419 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -79,12 +79,6 @@ def get_first_frame_padded(collection): return get_frame_padded(start_frame, padding=collection.padding) -def bulk_write(writes): - """Convenience function to bulk write into active project database""" - project = legacy_io.Session["AVALON_PROJECT"] - return legacy_io._database[project].bulk_write(writes) - - class IntegrateAsset(pyblish.api.InstancePlugin): """Register publish in the database and transfer files to destinations. @@ -288,7 +282,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Transaction to reduce the chances of another publish trying to # publish to the same version number since that chance can greatly # increase if the file transaction takes a long time. - bulk_write(subset_writes + version_writes) + legacy_io.bulk_write(subset_writes + version_writes) self.log.info("Subset {subset[name]} and Version {version[name]} " "written to database..".format(subset=subset, version=version)) @@ -362,7 +356,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): )) # Write representations to the database - bulk_write(representation_writes) + legacy_io.bulk_write(representation_writes) # Backwards compatibility # todo: can we avoid the need to store this? From 842cf06bf95458537d22eae3031bb7c64586e308 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 18:13:33 +0200 Subject: [PATCH 098/124] new integrator can tell legacy one that should not process the instance --- openpype/plugins/publish/integrate.py | 2 +- openpype/plugins/publish/integrate_legacy.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 790f96d419..71032a1d96 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -171,7 +171,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): template_name_profiles = None def process(self, instance): - + instance.data["processedWithNewIntegrator"] = True # Exclude instances that also contain families from exclude families families = set(get_instance_families(instance)) exclude = families & set(self.exclude_families) diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index 797479af45..18e4035602 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -145,6 +145,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): subset_grouping_profiles = None def process(self, instance): + if instance.data.get("processedWithNewIntegrator"): + self.log.info("Instance was already processed with new integrator") + return + for ef in self.exclude_families: if ( instance.data["family"] == ef or From a1784fc25e2b433e1f32e35a36588205b9904e98 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 18:26:27 +0200 Subject: [PATCH 099/124] representations are checked before instance registration begins --- openpype/plugins/publish/integrate.py | 80 +++++++++++++++++---------- 1 file changed, 50 insertions(+), 30 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 71032a1d96..97f99bdba7 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -14,6 +14,7 @@ from openpype.modules import ModulesManager from openpype.lib.profiles_filtering import filter_profiles from openpype.lib.file_transaction import FileTransaction from openpype.pipeline import legacy_io +from openpype.pipeline.publish import KnownPublishError log = logging.getLogger(__name__) @@ -172,6 +173,17 @@ class IntegrateAsset(pyblish.api.InstancePlugin): def process(self, instance): instance.data["processedWithNewIntegrator"] = True + + filtered_repres = self.filter_representations(instance) + # Skip instance if there are not representations to integrate + # all representations should not be integrated + if not filtered_repres: + self.log.warning(( + "Skipping, there are no representations" + " to integrate for instance {}" + ).format(instance.data["family"])) + return + # Exclude instances that also contain families from exclude families families = set(get_instance_families(instance)) exclude = families & set(self.exclude_families) @@ -182,7 +194,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): file_transactions = FileTransaction(log=self.log) try: - self.register(instance, file_transactions) + self.register(instance, file_transactions, filtered_repres) except Exception: # clean destination # todo: preferably we'd also rollback *any* changes to the database @@ -194,8 +206,35 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # the try, except. file_transactions.finalize() - def register(self, instance, file_transactions): + def filter_representations(self, instance): + # Prepare repsentations that should be integrated + repres = instance.data.get("representations") + # Raise error if instance don't have any representations + if not repres: + raise KnownPublishError( + "Instance {} has no representations to integrate".format( + instance.data["family"] + ) + ) + # Validate type of stored representations + if not isinstance(repres, (list, tuple)): + raise TypeError( + "Instance 'files' must be a list, got: {0} {1}".format( + str(type(repres)), str(repres) + ) + ) + + # Filter representations + filtered_repres = [] + for repre in repres: + if "delete" in repre.get("tags", []): + continue + filtered_repres.append(repre) + + return filtered_repres + + def register(self, instance, file_transactions, filtered_repres): instance_stagingdir = instance.data.get("stagingDir") if not instance_stagingdir: self.log.info(( @@ -209,15 +248,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "@ {0}".format(instance_stagingdir) ) - # Ensure at least one representation is set up for registering. - repres = instance.data.get("representations") - assert repres, "Instance has no representations data" - assert isinstance(repres, (list, tuple)), ( - "Instance 'representations' must be a list, got: {0} {1}".format( - str(type(repres)), str(repres) - ) - ) - template_name = self.get_template_name(instance) subset, subset_writes = self.prepare_subset(instance) @@ -238,20 +268,15 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Prepare all representations prepared_representations = [] - for repre in instance.data["representations"]: - - if "delete" in repre.get("tags", []): - self.log.debug("Skipping representation marked for deletion: " - "{}".format(repre)) - continue - + for repre in filtered_repres: # todo: reduce/simplify what is returned from this function - prepared = self.prepare_representation(repre, - template_name, - existing_repres_by_name, - version, - instance_stagingdir, - instance) + prepared = self.prepare_representation( + repre, + template_name, + existing_repres_by_name, + version, + instance_stagingdir, + instance) for src, dst in prepared["transfers"]: # todo: add support for hardlink transfers @@ -259,12 +284,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): prepared_representations.append(prepared) - if not prepared_representations: - # Even though we check `instance.data["representations"]` earlier - # this could still happen if all representations were tagged with - # "delete" and thus are skipped for integration - raise RuntimeError("No representations prepared to publish.") - # Each instance can also have pre-defined transfers not explicitly # part of a representation - like texture resources used by a # .ma representation. Those destination paths are pre-defined, etc. @@ -273,6 +292,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): for src, dst in instance.data.get("transfers", []): file_transactions.add(src, dst, mode=FileTransaction.MODE_COPY) resource_destinations.add(os.path.abspath(dst)) + for src, dst in instance.data.get("hardlinks", []): file_transactions.add(src, dst, mode=FileTransaction.MODE_HARDLINK) resource_destinations.add(os.path.abspath(dst)) From 7016ca41f7809a1d3729e4999ddf4c1c0dfe1299 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 18:27:37 +0200 Subject: [PATCH 100/124] use already prepared modules from context --- openpype/plugins/publish/integrate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 97f99bdba7..8fe5138963 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -317,8 +317,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.debug("Retrieving Representation Site Sync information ...") # Get the accessible sites for Site Sync - manager = ModulesManager() - sync_server_module = manager.modules_by_name["sync_server"] + modules_by_name = instance.context.data["openPypeModules"] + sync_server_module = modules_by_name["sync_server"] sites = sync_server_module.compute_resource_sync_sites( project_name=instance.data["projectEntity"]["name"] ) From d04abc3767f7c5990c6e0a8a420229bc034075f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 18:27:54 +0200 Subject: [PATCH 101/124] use host name from context data --- openpype/plugins/publish/integrate.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 8fe5138963..e76adb55b8 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -310,10 +310,10 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Process all file transfers of all integrations now self.log.debug("Integrating source files to destination ...") file_transactions.process() - self.log.debug("Backed up existing files: " - "{}".format(file_transactions.backups)) - self.log.debug("Transferred files: " - "{}".format(file_transactions.transferred)) + self.log.debug( + "Backed up existing files: {}".format(file_transactions.backups)) + self.log.debug( + "Transferred files: {}".format(file_transactions.transferred)) self.log.debug("Retrieving Representation Site Sync information ...") # Get the accessible sites for Site Sync @@ -780,8 +780,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): return { "families": anatomy_data["family"], "tasks": task.get("name"), - "hosts": anatomy_data["app"], - "task_types": task.get("type") + "task_types": task.get("type"), + "hosts": instance.context["hostName"], } def get_rootless_path(self, anatomy, path): From 79c01bdf1a83f4b5d4b57ce0afcc237f696a6387 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Jul 2022 18:28:29 +0200 Subject: [PATCH 102/124] skip instances marked to be integrated on farm --- openpype/plugins/publish/integrate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index e76adb55b8..e3a81091ba 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -172,8 +172,15 @@ class IntegrateAsset(pyblish.api.InstancePlugin): template_name_profiles = None def process(self, instance): + # Mark instance as processed for legacy integrator instance.data["processedWithNewIntegrator"] = True + # Instance should be integrated on a farm + if instance.data.get("farm"): + self.log.info( + "Instance is marked to be processed on farm. Skipping") + return + filtered_repres = self.filter_representations(instance) # Skip instance if there are not representations to integrate # all representations should not be integrated From f398fae425d66c1b9934e9ec016fa1634761f633 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Jul 2022 09:48:22 +0200 Subject: [PATCH 103/124] simplified settings for skipping of families --- .../defaults/project_settings/global.json | 119 +----------------- .../schemas/schema_global_publish.json | 60 +-------- 2 files changed, 7 insertions(+), 172 deletions(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index d923fc65c9..bdcf85d1b2 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -171,10 +171,6 @@ ] }, "IntegrateAssetNew": { - "hosts": [ - ], - "families": [ - ], "template_name_profiles": [ { "families": [], @@ -220,120 +216,7 @@ ] }, "IntegrateAsset": { - "hosts": [ - "maya", - "aftereffects", - "blender", - "celaction", - "flame", - "fusion", - "harmony", - "hiero", - "houdini", - "nuke", - "photoshop", - "resolve", - "tvpaint", - "unreal", - "standalonepublisher", - "webpublisher" - ], - "families": [ - "workfile", - "pointcache", - "camera", - "animation", - "model", - "mayaAscii", - "mayaScene", - "setdress", - "layout", - "ass", - "vdbcache", - "scene", - "vrayproxy", - "vrayscene_layer", - "render", - "prerender", - "imagesequence", - "review", - "rendersetup", - "rig", - "plate", - "look", - "audio", - "yetiRig", - "yeticache", - "nukenodes", - "gizmo", - "source", - "matchmove", - "image", - "assembly", - "fbx", - "textures", - "action", - "harmony.template", - "harmony.palette", - "editorial", - "background", - "camerarig", - "redshiftproxy", - "effect", - "xgen", - "hda", - "usd", - "staticMesh", - "skeletalMesh", - "mvLook", - "mvUsd", - "mvUsdComposition", - "mvUsdOverride", - "simpleUnrealTexture" - ], - "template_name_profiles": [ - { - "families": [], - "hosts": [], - "task_types": [], - "tasks": [], - "template_name": "publish" - }, - { - "families": [ - "review", - "render", - "prerender" - ], - "hosts": [], - "task_types": [], - "tasks": [], - "template_name": "render" - }, - { - "families": [ - "simpleUnrealTexture" - ], - "hosts": [ - "standalonepublisher" - ], - "task_types": [], - "tasks": [], - "template_name": "simpleUnrealTexture" - }, - { - "families": [ - "staticMesh", - "skeletalMesh" - ], - "hosts": [ - "maya" - ], - "task_types": [], - "tasks": [], - "template_name": "maya2unreal" - } - ] + "skip_host_families": [] }, "IntegrateHeroVersion": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 5e3978a2df..41eb04be4e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -587,18 +587,6 @@ "label": "IntegrateAsset (Legacy)", "is_group": true, "children": [ - { - "type": "list", - "key": "hosts", - "label": "Hosts", - "object_type": "text" - }, - { - "key": "families", - "label": "Families", - "type": "list", - "object_type": "text" - }, { "type": "list", "key": "template_name_profiles", @@ -656,58 +644,22 @@ "children": [ { "type": "list", - "key": "hosts", - "label": "Hosts", - "object_type": "text" - }, - { - "key": "families", - "label": "Families", - "type": "list", - "object_type": "text" - }, - { - "type": "list", - "key": "template_name_profiles", - "label": "Template name profiles", + "key": "skip_host_families", + "label": "Skip hosts and families", "use_label_wrap": true, "object_type": { "type": "dict", "children": [ { - "type": "label", - "label": "" + "type": "hosts-enum", + "key": "host", + "label": "Host" }, { + "type": "list", "key": "families", "label": "Families", - "type": "list", "object_type": "text" - }, - { - "type": "hosts-enum", - "key": "hosts", - "label": "Hosts", - "multiselection": true - }, - { - "key": "task_types", - "label": "Task types", - "type": "task-types-enum" - }, - { - "key": "tasks", - "label": "Task names", - "type": "list", - "object_type": "text" - }, - { - "type": "separator" - }, - { - "type": "text", - "key": "template_name", - "label": "Template name" } ] } From 1a024d3552723245c273362793ecee6b99f29823 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Jul 2022 09:56:52 +0200 Subject: [PATCH 104/124] use settings to decide if new integrator should skip instances --- openpype/plugins/publish/integrate.py | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index e3a81091ba..0b725750aa 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -167,11 +167,15 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "project", "asset", "task", "subset", "version", "representation", "family", "hierarchy", "username" ] + skip_host_families = [] # Attributes set by settings template_name_profiles = None def process(self, instance): + if self._temp_skip_instance_by_settings(instance): + return + # Mark instance as processed for legacy integrator instance.data["processedWithNewIntegrator"] = True @@ -213,6 +217,39 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # the try, except. file_transactions.finalize() + def _temp_skip_instance_by_settings(self, instance): + """Decide if instance will be processed with new or legacy integrator. + + This is temporary solution until we test all usecases with new (this) + integrator plugin. + """ + + host_name = instance.context.data["hostName"] + instance_family = instance.data["family"] + instance_families = set(instance.data.get("families") or []) + + skip = False + for item in self.skip_host_families: + if item["host"] != host_name: + continue + + families = set(item["families"]) + if instance_family in families: + skip = True + break + + for family in instance_families: + if family in families: + skip = True + break + + if skip: + break + + if skip: + self.log.debug("Instance is marked to be skipped by settings.") + return skip + def filter_representations(self, instance): # Prepare repsentations that should be integrated repres = instance.data.get("representations") From 0ebd4b7c9afffa3174ae876f94ca22fa52f83627 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Jul 2022 09:57:00 +0200 Subject: [PATCH 105/124] added remaining hosts to integrator hosts --- openpype/plugins/publish/integrate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 0b725750aa..3c61d01858 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -105,7 +105,10 @@ class IntegrateAsset(pyblish.api.InstancePlugin): label = "Integrate Asset" order = pyblish.api.IntegratorOrder - hosts = ["maya"] + hosts = ["aftereffects", "blender", "celaction", "flame", "harmony", + "hiero", "houdini", "nuke", "photoshop", "resolve", + "standalonepublisher", "traypublisher", "tvpaint", "unreal", + "webpublisher"] families = ["workfile", "pointcache", "camera", From 6c457b2ed1331bb193a572803670836b40f16667 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Jul 2022 10:10:22 +0200 Subject: [PATCH 106/124] use settings for publish templates from legacy integrator --- openpype/plugins/publish/integrate.py | 33 ++++++++++++++++++++------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 3c61d01858..1b8015c946 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -172,9 +172,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): ] skip_host_families = [] - # Attributes set by settings - template_name_profiles = None - def process(self, instance): if self._temp_skip_instance_by_settings(instance): return @@ -807,13 +804,33 @@ class IntegrateAsset(pyblish.api.InstancePlugin): """Return anatomy template name to use for integration""" # Define publish template name from profiles filter_criteria = self.get_profile_filter_criteria(instance) - profile = filter_profiles(self.template_name_profiles, - filter_criteria, - logger=self.log) + template_name_profiles = self._get_template_name_profiles(instance) + profile = filter_profiles( + template_name_profiles, + filter_criteria, + logger=self.log + ) + if profile: return profile["template_name"] - else: - return self.default_template_name + return self.default_template_name + + def _get_template_name_profiles(self, instance): + """Receive profiles for publish template keys. + + Reuse template name profiles from legacy integrator. Goal is to move + the profile settings out of plugin settings but until that happens we + want to be able set it at one place and don't break backwards + compatibility (more then once). + """ + + return ( + instance.context["project_settings"] + ["global"] + ["publish"] + ["IntegrateAssetNew"] + ["template_name_profiles"] + ) def get_profile_filter_criteria(self, instance): """Return filter criteria for `filter_profiles`""" From 30db574170a526759ac6c40c574aac0ed41bcdea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Jul 2022 10:18:41 +0200 Subject: [PATCH 107/124] fixed data access --- openpype/plugins/publish/integrate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 1b8015c946..c9eb26d0b7 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -825,7 +825,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): """ return ( - instance.context["project_settings"] + instance.context.data["project_settings"] ["global"] ["publish"] ["IntegrateAssetNew"] @@ -845,7 +845,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "families": anatomy_data["family"], "tasks": task.get("name"), "task_types": task.get("type"), - "hosts": instance.context["hostName"], + "hosts": instance.context.data["hostName"], } def get_rootless_path(self, anatomy, path): From 3c35cbc700102270c55adfa8b249ad9f75ebd226 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Jul 2022 10:23:13 +0200 Subject: [PATCH 108/124] make sure legacy integrator happens after new integrator --- openpype/plugins/publish/integrate.py | 1 - openpype/plugins/publish/integrate_legacy.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index c9eb26d0b7..cfaff4067b 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -10,7 +10,6 @@ from pymongo import DeleteMany, ReplaceOne, InsertOne, UpdateOne import pyblish.api import openpype.api -from openpype.modules import ModulesManager from openpype.lib.profiles_filtering import filter_profiles from openpype.lib.file_transaction import FileTransaction from openpype.pipeline import legacy_io diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index 18e4035602..34e81a3839 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -70,7 +70,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): """ label = "Integrate Asset (legacy)" - order = pyblish.api.IntegratorOrder + # Make sure it happens after new integrator + order = pyblish.api.IntegratorOrder + 0.00001 hosts = ["aftereffects", "blender", "celaction", "flame", "harmony", "hiero", "houdini", "nuke", "photoshop", "resolve", "standalonepublisher", "traypublisher", "tvpaint", "unreal", From dc569c6d65e2a211681a4f0d17aea1874cef9268 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Jul 2022 10:30:31 +0200 Subject: [PATCH 109/124] rename and move CollectSubsetGroup to IntegrateSubsetGroup in settings --- .../defaults/project_settings/global.json | 22 ++-- .../schemas/schema_global_publish.json | 110 +++++++++--------- 2 files changed, 66 insertions(+), 66 deletions(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index bdcf85d1b2..9247c6ceb6 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -20,17 +20,6 @@ ], "skip_hosts_headless_publish": [] }, - "CollectSubsetGroup": { - "subset_grouping_profiles": [ - { - "families": [], - "hosts": [], - "task_types": [], - "tasks": [], - "template": "" - } - ] - }, "ValidateEditorialAssetName": { "enabled": true, "optional": false @@ -170,6 +159,17 @@ } ] }, + "IntegrateSubsetGroup": { + "subset_grouping_profiles": [ + { + "families": [], + "hosts": [], + "task_types": [], + "tasks": [], + "template": "" + } + ] + }, "IntegrateAssetNew": { "template_name_profiles": [ { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 41eb04be4e..af08bbec3c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -39,61 +39,6 @@ } ] }, - { - "type": "dict", - "collapsible": true, - "key": "CollectSubsetGroup", - "label": "Collect Subset Group", - "is_group": true, - "children": [ - { - "type": "list", - "key": "subset_grouping_profiles", - "label": "Subset grouping profiles", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "label", - "label": "Set all published instances as a part of specific group named according to 'Template'.
Implemented all variants of placeholders [{task},{family},{host},{subset},{renderlayer}]" - }, - { - "key": "families", - "label": "Families", - "type": "list", - "object_type": "text" - }, - { - "type": "hosts-enum", - "key": "hosts", - "label": "Hosts", - "multiselection": true - }, - { - "key": "task_types", - "label": "Task types", - "type": "task-types-enum" - }, - { - "key": "tasks", - "label": "Task names", - "type": "list", - "object_type": "text" - }, - { - "type": "separator" - }, - { - "type": "text", - "key": "template", - "label": "Template" - } - ] - } - } - ] - }, { "type": "dict", "collapsible": true, @@ -580,6 +525,61 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "IntegrateSubsetGroup", + "label": "Integrate Subset Group", + "is_group": true, + "children": [ + { + "type": "list", + "key": "subset_grouping_profiles", + "label": "Subset grouping profiles", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "label", + "label": "Set all published instances as a part of specific group named according to 'Template'.
Implemented all variants of placeholders [{task},{family},{host},{subset},{renderlayer}]" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "type": "hosts-enum", + "key": "hosts", + "label": "Hosts", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "template", + "label": "Template" + } + ] + } + } + ] + }, { "type": "dict", "collapsible": true, From 76c594af9a39f22bbbb00156c09d67bd9d0d2d0b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Jul 2022 10:30:48 +0200 Subject: [PATCH 110/124] check for existence of subset group on instance before profiling --- .../plugins/publish/integrate_subset_group.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/openpype/plugins/publish/integrate_subset_group.py b/openpype/plugins/publish/integrate_subset_group.py index 4b566e8908..910cb060a6 100644 --- a/openpype/plugins/publish/integrate_subset_group.py +++ b/openpype/plugins/publish/integrate_subset_group.py @@ -37,18 +37,22 @@ class IntegrateSubsetGroup(pyblish.api.InstancePlugin): if not self.subset_grouping_profiles: return - # Skip if there is no matching profile - filter_criteria = self.get_profile_filter_criteria(instance) - profile = filter_profiles(self.subset_grouping_profiles, - filter_criteria, - logger=self.log) - if not profile: - return - if instance.data.get("subsetGroup"): # If subsetGroup is already set then allow that value to remain - self.log.debug("Skipping collect subset group due to existing " - "value: {}".format(instance.data["subsetGroup"])) + self.log.debug(( + "Skipping collect subset group due to existing value: {}" + ).format(instance.data["subsetGroup"])) + return + + # Skip if there is no matching profile + filter_criteria = self.get_profile_filter_criteria(instance) + profile = filter_profiles( + self.subset_grouping_profiles, + filter_criteria, + logger=self.log + ) + + if not profile: return template = profile["template"] @@ -68,9 +72,9 @@ class IntegrateSubsetGroup(pyblish.api.InstancePlugin): ) except (KeyError, TemplateUnsolved): keys = fill_pairs.keys() - msg = "Subset grouping failed. " \ - "Only {} are expected in Settings".format(','.join(keys)) - self.log.warning(msg) + self.log.warning(( + "Subset grouping failed. Only {} are expected in Settings" + ).format(','.join(keys))) if filled_template: instance.data["subsetGroup"] = filled_template From 037ed71f60eba9d5947aef8c973e8b791596b942 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Jul 2022 13:52:15 +0200 Subject: [PATCH 111/124] keep subset group template settings but mark them as deprecated with hint where to move the value --- .../defaults/project_settings/global.json | 9 ++++ .../schemas/schema_global_publish.json | 46 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 9247c6ceb6..e509db2791 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -171,6 +171,15 @@ ] }, "IntegrateAssetNew": { + "subset_grouping_profiles": [ + { + "families": [], + "hosts": [], + "task_types": [], + "tasks": [], + "template": "" + } + ], "template_name_profiles": [ { "families": [], diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index af08bbec3c..b9d0b7daba 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -587,6 +587,52 @@ "label": "IntegrateAsset (Legacy)", "is_group": true, "children": [ + { + "type": "label", + "label": "NOTE: Subset grouping profiles settings were moved to
Integrate Subset Group. Please move values there." + }, + { + "type": "list", + "key": "subset_grouping_profiles", + "label": "Subset grouping profiles (DEPRECATED)", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "type": "hosts-enum", + "key": "hosts", + "label": "Hosts", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "template", + "label": "Template" + } + ] + } + }, { "type": "list", "key": "template_name_profiles", From a7044dadf7c74260f5c8945cccad196d4de89a1e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Jul 2022 17:35:27 +0200 Subject: [PATCH 112/124] fix containers varible usage --- openpype/pipeline/load/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index a9aa240ff6..8b12088d3c 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -732,7 +732,7 @@ def filter_containers(containers, project_name): some missing entity in database. Args: - containers (list[dict]): List of containers referenced into scene. + containers (Iterable[dict]): List of containers referenced into scene. project_name (str): Name of project in which context shoud look for versions. @@ -741,6 +741,9 @@ def filter_containers(containers, project_name): 'invalid' and 'not_found' containers. """ + # Make sure containers is list that won't change + containers = list(containers) + outdated_containers = [] uptodate_containers = [] not_found_containers = [] From 2209bcf6b10f4e65a25e58b34373178aa8b92648 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Jul 2022 11:31:44 +0200 Subject: [PATCH 113/124] check for 'ILoadHost' to call different method on host --- openpype/pipeline/load/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 8b12088d3c..fe5102353d 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -7,6 +7,7 @@ import inspect import collections import numbers +from openpype.host import ILoadHost from openpype.client import ( get_project, get_assets, @@ -719,7 +720,11 @@ def get_outdated_containers(host=None, project_name=None): if project_name is None: project_name = legacy_io.active_project() - containers = host.ls() + + if isinstance(host, ILoadHost): + containers = host.get_containers() + else: + containers = host.ls() return filter_containers(containers, project_name).outdated From 78b4bbadc92ec29167af3487e1b597e07a40f35e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 22 Jul 2022 11:39:22 +0200 Subject: [PATCH 114/124] add continuos arguments next to each other --- openpype/plugins/publish/extract_review_slate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 2edaf10e6b..28deb360be 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -295,12 +295,14 @@ class ExtractReviewSlate(openpype.api.Extractor): # this will reencode the output if input_audio: fmap = [ + "-filter_complex", "[0:v] [0:a] [1:v] [1:a] concat=n=2:v=1:a=1 [v] [a]", "-map", '[v]', "-map", '[a]' ] else: fmap = [ + "-filter_complex", "[0:v] [1:v] concat=n=2:v=1:a=0 [v]", "-map", '[v]' ] @@ -308,7 +310,6 @@ class ExtractReviewSlate(openpype.api.Extractor): ffmpeg_path, "-i", slate_v_path, "-i", input_path, - "-filter_complex", ] concat_args.extend(fmap) if offset_timecode: From f99f811ddd4194caadd27a481aef766eae2e5727 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 22 Jul 2022 11:39:37 +0200 Subject: [PATCH 115/124] add `-y` into base of ffmpeg arguments --- openpype/plugins/publish/extract_review_slate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 28deb360be..90dad00b97 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -308,6 +308,7 @@ class ExtractReviewSlate(openpype.api.Extractor): ] concat_args = [ ffmpeg_path, + "-y", "-i", slate_v_path, "-i", input_path, ] @@ -319,6 +320,7 @@ class ExtractReviewSlate(openpype.api.Extractor): # - keep format of output if format_args: concat_args.extend(format_args) + # Use arguments from ffmpeg preset source_ffmpeg_cmd = repre.get("ffmpeg_cmd") if source_ffmpeg_cmd: @@ -334,7 +336,7 @@ class ExtractReviewSlate(openpype.api.Extractor): concat_args.append(arg) # assumes arg has one parameter concat_args.append(args[indx + 1]) - concat_args.append("-y") + # add final output path concat_args.append(output_path) From fcbf46d345bcef7363bc4f590d476136f478b6ce Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 22 Jul 2022 14:20:56 +0200 Subject: [PATCH 116/124] Add normaal animation --- website/src/css/custom.css | 4 ++-- website/src/pages/index.js | 16 ++++++++++------ website/static/img/logo_normaal.png | Bin 0 -> 13468 bytes 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 website/static/img/logo_normaal.png diff --git a/website/src/css/custom.css b/website/src/css/custom.css index e8dd86256b..58c9305bc7 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -196,12 +196,12 @@ html[data-theme='dark'] .header-github-link::before { padding: 20px } -.showcase .client { +.showcase .studio { display: flex; justify-content: space-between; } -.showcase .client img { +.showcase .studio img { max-height: 110px; padding: 20px; max-width: 160px; diff --git a/website/src/pages/index.js b/website/src/pages/index.js index ae7119e928..52302ec285 100644 --- a/website/src/pages/index.js +++ b/website/src/pages/index.js @@ -65,13 +65,17 @@ const collab = [ image: '/img/clothcat.png', infoLink: 'https://www.clothcatanimation.com/' }, { - title: 'Ellipse Studio', - image: '/img/ellipse-studio.png', - infoLink: 'http://www.dargaudmedia.com' + title: 'Ellipse Animation', + image: '/img/ellipse_animation.svg', + infoLink: 'http://www.ellipseanimation.com' }, { title: 'J Cube Inc', image: '/img/jcube_logo_bw.png', infoLink: 'https://j-cube.jp' + }, { + title: 'Normaal Animation', + image: '/img/logo_normaal.png', + infoLink: 'https://j-cube.jp' } ]; @@ -191,10 +195,10 @@ function Service({imageUrl, title, description}) { ); } -function Client({title, image, infoLink}) { +function Studio({title, image, infoLink}) { const imgUrl = useBaseUrl(image); return ( - + ); @@ -490,7 +494,7 @@ function Home() {

Studios using openPype

{studios.map((props, idx) => ( - + ))}
diff --git a/website/static/img/logo_normaal.png b/website/static/img/logo_normaal.png new file mode 100644 index 0000000000000000000000000000000000000000..711847c9f2f95d77d46d4ab98a9287e5f0ef771d GIT binary patch literal 13468 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 mxrUV7^Rp0{u+ Date: Fri, 22 Jul 2022 14:21:06 +0200 Subject: [PATCH 117/124] update Ellipse animation after re-brand --- website/static/img/ellipse_animation.svg | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 website/static/img/ellipse_animation.svg diff --git a/website/static/img/ellipse_animation.svg b/website/static/img/ellipse_animation.svg new file mode 100644 index 0000000000..c1caaa6726 --- /dev/null +++ b/website/static/img/ellipse_animation.svg @@ -0,0 +1,9 @@ + + + + + + + + + From 69246a76b4d6e494c563c828bdfb203fd0e80c44 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 22 Jul 2022 15:01:31 +0200 Subject: [PATCH 118/124] fixing the host condition --- openpype/plugins/publish/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index cfaff4067b..1ddb694f85 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -229,7 +229,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): skip = False for item in self.skip_host_families: - if item["host"] != host_name: + if host_name not in item["host"]: continue families = set(item["families"]) From abc5c9e69b6cbdd0627d229e7e8294c159cef0e0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 22 Jul 2022 15:37:10 +0200 Subject: [PATCH 119/124] adding codec args for keeping continuity even wtih audio stream. --- openpype/plugins/publish/extract_review_slate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 90dad00b97..69043ee261 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -321,6 +321,9 @@ class ExtractReviewSlate(openpype.api.Extractor): if format_args: concat_args.extend(format_args) + if codec_args: + concat_args.extend(codec_args) + # Use arguments from ffmpeg preset source_ffmpeg_cmd = repre.get("ffmpeg_cmd") if source_ffmpeg_cmd: From 4ac8da4ca047363005b1f0638c29584f847c8590 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 22 Jul 2022 18:14:26 +0200 Subject: [PATCH 120/124] remove hosts filter on integrator plugins --- openpype/plugins/publish/integrate.py | 4 ---- openpype/plugins/publish/integrate_legacy.py | 4 ---- 2 files changed, 8 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 1ddb694f85..8532691e61 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -104,10 +104,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): label = "Integrate Asset" order = pyblish.api.IntegratorOrder - hosts = ["aftereffects", "blender", "celaction", "flame", "harmony", - "hiero", "houdini", "nuke", "photoshop", "resolve", - "standalonepublisher", "traypublisher", "tvpaint", "unreal", - "webpublisher"] families = ["workfile", "pointcache", "camera", diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index 34e81a3839..b90b61f587 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -72,10 +72,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): label = "Integrate Asset (legacy)" # Make sure it happens after new integrator order = pyblish.api.IntegratorOrder + 0.00001 - hosts = ["aftereffects", "blender", "celaction", "flame", "harmony", - "hiero", "houdini", "nuke", "photoshop", "resolve", - "standalonepublisher", "traypublisher", "tvpaint", "unreal", - "webpublisher"] families = ["workfile", "pointcache", "camera", From 5c0f0f260365423128e410712c18c1938e83777b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 22 Jul 2022 18:22:48 +0200 Subject: [PATCH 121/124] :heavy_minus_sign: remove invalid submodules --- vendor/powershell/BurntToast | 1 - vendor/powershell/PSWriteColor | 1 - 2 files changed, 2 deletions(-) delete mode 160000 vendor/powershell/BurntToast delete mode 160000 vendor/powershell/PSWriteColor diff --git a/vendor/powershell/BurntToast b/vendor/powershell/BurntToast deleted file mode 160000 index ae0acdd870..0000000000 --- a/vendor/powershell/BurntToast +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ae0acdd870a2fd8d9f0d147de22dc36d6c5e399e diff --git a/vendor/powershell/PSWriteColor b/vendor/powershell/PSWriteColor deleted file mode 160000 index 12eda384eb..0000000000 --- a/vendor/powershell/PSWriteColor +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 12eda384ebd7a7954e15855e312215c009c97114 From e69d8e3ac65ee273e7d9a23c4bbd5f741baf01c9 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 23 Jul 2022 03:53:23 +0000 Subject: [PATCH 122/124] [Automated] Bump version --- CHANGELOG.md | 35 ++++++++++++++++++----------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8da885473..ec880b9c61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,17 @@ # Changelog -## [3.12.2-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.12.2-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.1...HEAD) +### 📖 Documentation + +- Update website with more studios [\#3554](https://github.com/pypeclub/OpenPype/pull/3554) +- Documentation: Update publishing dev docs [\#3549](https://github.com/pypeclub/OpenPype/pull/3549) + **🚀 Enhancements** +- Maya: add additional validators to Settings [\#3540](https://github.com/pypeclub/OpenPype/pull/3540) - General: Interactive console in cli [\#3526](https://github.com/pypeclub/OpenPype/pull/3526) - Ftrack: Automatic daily review session creation can define trigger hour [\#3516](https://github.com/pypeclub/OpenPype/pull/3516) - Ftrack: add source into Note [\#3509](https://github.com/pypeclub/OpenPype/pull/3509) @@ -20,8 +26,15 @@ **🐛 Bug fixes** +- Remove invalid submodules from `/vendor` [\#3557](https://github.com/pypeclub/OpenPype/pull/3557) +- General: Remove hosts filter on integrator plugins [\#3556](https://github.com/pypeclub/OpenPype/pull/3556) +- Settings: Clean default values of environments [\#3550](https://github.com/pypeclub/OpenPype/pull/3550) +- Module interfaces: Fix import error [\#3547](https://github.com/pypeclub/OpenPype/pull/3547) +- Workfiles tool: Show of tool and it's flags [\#3539](https://github.com/pypeclub/OpenPype/pull/3539) +- General: Create workfile documents works again [\#3538](https://github.com/pypeclub/OpenPype/pull/3538) - Additional fixes for powershell scripts [\#3525](https://github.com/pypeclub/OpenPype/pull/3525) - Maya: Added wrapper around cmds.setAttr [\#3523](https://github.com/pypeclub/OpenPype/pull/3523) +- Nuke: double slate [\#3521](https://github.com/pypeclub/OpenPype/pull/3521) - General: Fix hash of centos oiio archive [\#3519](https://github.com/pypeclub/OpenPype/pull/3519) - Maya: Renderman display output fix [\#3514](https://github.com/pypeclub/OpenPype/pull/3514) - TrayPublisher: Simple creation enhancements and fixes [\#3513](https://github.com/pypeclub/OpenPype/pull/3513) @@ -31,8 +44,12 @@ **🔀 Refactored code** +- Refactor Integrate Asset [\#3530](https://github.com/pypeclub/OpenPype/pull/3530) - General: Client docstrings cleanup [\#3529](https://github.com/pypeclub/OpenPype/pull/3529) +- General: Get current context document functions [\#3522](https://github.com/pypeclub/OpenPype/pull/3522) +- Kitsu: Use query function from client [\#3496](https://github.com/pypeclub/OpenPype/pull/3496) - TimersManager: Use query functions [\#3495](https://github.com/pypeclub/OpenPype/pull/3495) +- Deadline: Use query functions [\#3466](https://github.com/pypeclub/OpenPype/pull/3466) ## [3.12.1](https://github.com/pypeclub/OpenPype/tree/3.12.1) (2022-07-13) @@ -57,7 +74,6 @@ - Windows installer: Clean old files and add version subfolder [\#3445](https://github.com/pypeclub/OpenPype/pull/3445) - Blender: Bugfix - Set fps properly on open [\#3426](https://github.com/pypeclub/OpenPype/pull/3426) - Hiero: Add custom scripts menu [\#3425](https://github.com/pypeclub/OpenPype/pull/3425) -- Blender: pre pyside install for all platforms [\#3400](https://github.com/pypeclub/OpenPype/pull/3400) **🐛 Bug fixes** @@ -95,34 +111,19 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.12.0-nightly.3...3.12.0) -### 📖 Documentation - -- Fix typo in documentation: pyenv on mac [\#3417](https://github.com/pypeclub/OpenPype/pull/3417) -- Linux: update OIIO package [\#3401](https://github.com/pypeclub/OpenPype/pull/3401) - **🚀 Enhancements** - Webserver: Added CORS middleware [\#3422](https://github.com/pypeclub/OpenPype/pull/3422) -- Attribute Defs UI: Files widget show what is allowed to drop in [\#3411](https://github.com/pypeclub/OpenPype/pull/3411) **🐛 Bug fixes** - NewPublisher: Fix subset name change on change of creator plugin [\#3420](https://github.com/pypeclub/OpenPype/pull/3420) - Bug: fix invalid avalon import [\#3418](https://github.com/pypeclub/OpenPype/pull/3418) -- Nuke: Fix keyword argument in query function [\#3414](https://github.com/pypeclub/OpenPype/pull/3414) -- Houdini: fix loading and updating vbd/bgeo sequences [\#3408](https://github.com/pypeclub/OpenPype/pull/3408) -- Nuke: Collect representation files based on Write [\#3407](https://github.com/pypeclub/OpenPype/pull/3407) -- General: Filter representations before integration start [\#3398](https://github.com/pypeclub/OpenPype/pull/3398) -- Maya: look collector typo [\#3392](https://github.com/pypeclub/OpenPype/pull/3392) **🔀 Refactored code** - Unreal: Use client query functions [\#3421](https://github.com/pypeclub/OpenPype/pull/3421) - General: Move editorial lib to pipeline [\#3419](https://github.com/pypeclub/OpenPype/pull/3419) -- Kitsu: renaming to plural func sync\_all\_projects [\#3397](https://github.com/pypeclub/OpenPype/pull/3397) -- Houdini: Use client query functions [\#3395](https://github.com/pypeclub/OpenPype/pull/3395) -- Hiero: Use client query functions [\#3393](https://github.com/pypeclub/OpenPype/pull/3393) -- Nuke: Use client query functions [\#3391](https://github.com/pypeclub/OpenPype/pull/3391) ## [3.11.1](https://github.com/pypeclub/OpenPype/tree/3.11.1) (2022-06-20) diff --git a/openpype/version.py b/openpype/version.py index dd5ad97449..9dda1eacce 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.12.2-nightly.2" +__version__ = "3.12.2-nightly.3" diff --git a/pyproject.toml b/pyproject.toml index 9552242694..eebc8a5600 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.12.2-nightly.2" # OpenPype +version = "3.12.2-nightly.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From a96bfc45ad05c14267ef7d6cd968ab412cf00172 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 25 Jul 2022 13:49:15 +0200 Subject: [PATCH 123/124] Fix - method expects dict not id --- openpype/hosts/harmony/plugins/load/load_background.py | 2 +- openpype/hosts/harmony/plugins/load/load_imagesequence.py | 2 +- openpype/hosts/harmony/plugins/load/load_template.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/harmony/plugins/load/load_background.py b/openpype/hosts/harmony/plugins/load/load_background.py index 9e9fcbfa32..c28a87791e 100644 --- a/openpype/hosts/harmony/plugins/load/load_background.py +++ b/openpype/hosts/harmony/plugins/load/load_background.py @@ -300,7 +300,7 @@ class BackgroundLoader(load.LoaderPlugin): print(container) - is_latest = is_representation_from_latest(representation["parent"]) + is_latest = is_representation_from_latest(representation) for layer in sorted(layers): file_to_import = [ os.path.join(bg_folder, layer).replace("\\", "/") diff --git a/openpype/hosts/harmony/plugins/load/load_imagesequence.py b/openpype/hosts/harmony/plugins/load/load_imagesequence.py index 8d6421a6aa..1b64aff595 100644 --- a/openpype/hosts/harmony/plugins/load/load_imagesequence.py +++ b/openpype/hosts/harmony/plugins/load/load_imagesequence.py @@ -109,7 +109,7 @@ class ImageSequenceLoader(load.LoaderPlugin): ) # Colour node. - if is_representation_from_latest(representation["parent"]): + if is_representation_from_latest(representation): harmony.send( { "function": "PypeHarmony.setColor", diff --git a/openpype/hosts/harmony/plugins/load/load_template.py b/openpype/hosts/harmony/plugins/load/load_template.py index 8ddd3934f7..f3c69a9104 100644 --- a/openpype/hosts/harmony/plugins/load/load_template.py +++ b/openpype/hosts/harmony/plugins/load/load_template.py @@ -83,7 +83,7 @@ class TemplateLoader(load.LoaderPlugin): self_name = self.__class__.__name__ update_and_replace = False - if is_representation_from_latest(representation["parent"]): + if is_representation_from_latest(representation): self._set_green(node) else: self._set_red(node) From e8bfbf4292979f54ac82114b967394392eefa0a1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 25 Jul 2022 16:44:34 +0200 Subject: [PATCH 124/124] Added validator for old containers for AfterEffects --- openpype/plugins/publish/validate_containers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/validate_containers.py b/openpype/plugins/publish/validate_containers.py index 7732ec5ea9..b2a3ed9b79 100644 --- a/openpype/plugins/publish/validate_containers.py +++ b/openpype/plugins/publish/validate_containers.py @@ -19,7 +19,7 @@ class ValidateContainers(pyblish.api.ContextPlugin): label = "Validate Containers" order = pyblish.api.ValidatorOrder - hosts = ["maya", "houdini", "nuke", "harmony", "photoshop"] + hosts = ["maya", "houdini", "nuke", "harmony", "photoshop", "aftereffects"] optional = True actions = [ShowInventory]

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 literal 80331 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 z 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 CA{hw) diff --git a/website/static/img/methodmadness.png b/website/static/img/methodmadness.png new file mode 100644 index 0000000000000000000000000000000000000000..9dd0681d4a029b11c8db74154934333db42e1851 GIT binary patch literal 8650 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 literal 0 HcmV?d00001 diff --git a/website/static/img/noghost.png b/website/static/img/noghost.png new file mode 100644 index 0000000000000000000000000000000000000000..febaedcae8212926ae8d7eb3705c8304531f8375 GIT binary patch literal 22435 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