From d88ed919e6a4a566b8ff8b289415c471de454b00 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 16 Mar 2022 22:09:23 +0100 Subject: [PATCH 001/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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/114] 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 9ef9c79e8fb7cf6e2a783abe3eba1ba13f1eaa6d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 18 Jul 2022 11:34:30 +0200 Subject: [PATCH 083/114] 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 084/114] 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 085/114] 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 086/114] 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 087/114] 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 088/114] 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 089/114] 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 090/114] 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 091/114] 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 092/114] 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 093/114] 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 094/114] 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 095/114] 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 096/114] 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 097/114] 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 098/114] 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 099/114] 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 100/114] 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 bedca7eaf98212d1a5450b097029d25dfdaf4d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 20 Jul 2022 16:30:03 +0200 Subject: [PATCH 101/114] :wrench: add relevant Maya validators to Settings add missing validators and add ability to set them optional if needed --- .../validate_review_subset_uniqueness.py | 4 +- .../plugins/publish/validate_setdress_root.py | 3 +- .../defaults/project_settings/maya.json | 178 +++++++++++++++++- .../schemas/schema_maya_publish.json | 155 +++++++++++++++ 4 files changed, 329 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_review_subset_uniqueness.py b/openpype/hosts/maya/plugins/publish/validate_review_subset_uniqueness.py index d70096ee45..04cc9ab5fb 100644 --- a/openpype/hosts/maya/plugins/publish/validate_review_subset_uniqueness.py +++ b/openpype/hosts/maya/plugins/publish/validate_review_subset_uniqueness.py @@ -6,7 +6,7 @@ from openpype.pipeline import PublishXmlValidationError class ValidateReviewSubsetUniqueness(pyblish.api.ContextPlugin): - """Validates that nodes has common root.""" + """Validates that review subset has unique name.""" order = openpype.api.ValidateContentsOrder hosts = ["maya"] @@ -17,7 +17,7 @@ class ValidateReviewSubsetUniqueness(pyblish.api.ContextPlugin): subset_names = [] for instance in context: - self.log.info("instance:: {}".format(instance.data)) + self.log.debug("Instance: {}".format(instance.data)) if instance.data.get('publish'): subset_names.append(instance.data.get('subset')) diff --git a/openpype/hosts/maya/plugins/publish/validate_setdress_root.py b/openpype/hosts/maya/plugins/publish/validate_setdress_root.py index 0b4842d208..8e23a7c04f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_setdress_root.py +++ b/openpype/hosts/maya/plugins/publish/validate_setdress_root.py @@ -4,8 +4,7 @@ import openpype.api class ValidateSetdressRoot(pyblish.api.InstancePlugin): - """ - """ + """Validate if set dress top root node is published.""" order = openpype.api.ValidateContentsOrder label = "SetDress Root" diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 5976c6a823..c96acbff6d 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -205,10 +205,15 @@ "enabled": true, "optional": true, "active": true, - "exclude_families": ["model", "rig", "staticMesh"] + "exclude_families": [ + "model", + "rig", + "staticMesh" + ] }, "ValidateShaderName": { "enabled": false, + "optional": true, "regex": "(?P.*)_(.*)_SHD" }, "ValidateShadingEngine": { @@ -222,6 +227,7 @@ }, "ValidateLoadedPlugin": { "enabled": false, + "optional": true, "whitelist_native_plugins": false, "authorized_plugins": [] }, @@ -236,6 +242,7 @@ }, "ValidateUnrealStaticMeshName": { "enabled": true, + "optional": true, "validate_mesh": false, "validate_collision": true }, @@ -252,6 +259,81 @@ "redshift_render_attributes": [], "renderman_render_attributes": [] }, + "ValidateCurrentRenderLayerIsRenderable": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateRenderImageRule": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateRenderNoDefaultCameras": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateRenderSingleCamera": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateRenderLayerAOVs": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateStepSize": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateVRayDistributedRendering": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateVrayReferencedAOVs": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateVRayTranslatorEnabled": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateVrayProxy": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateVrayProxyMembers": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateYetiRenderScriptCallbacks": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateYetiRigCacheState": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateYetiRigInputShapesInInstance": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateYetiRigSettings": { + "enabled": true, + "optional": false, + "active": true + }, "ValidateModelName": { "enabled": false, "database": true, @@ -270,6 +352,7 @@ }, "ValidateTransformNamingSuffix": { "enabled": true, + "optional": true, "SUFFIX_NAMING_TABLE": { "mesh": [ "_GEO", @@ -293,7 +376,7 @@ "ALLOW_IF_NOT_IN_SUFFIX_TABLE": true }, "ValidateColorSets": { - "enabled": false, + "enabled": true, "optional": true, "active": true }, @@ -337,6 +420,16 @@ "optional": true, "active": true }, + "ValidateMeshNoNegativeScale": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateMeshNonZeroEdgeLength": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateMeshNormalsUnlocked": { "enabled": false, "optional": true, @@ -359,22 +452,22 @@ }, "ValidateNoNamespace": { "enabled": true, - "optional": true, + "optional": false, "active": true }, "ValidateNoNullTransforms": { "enabled": true, - "optional": true, + "optional": false, "active": true }, "ValidateNoUnknownNodes": { "enabled": true, - "optional": true, + "optional": false, "active": true }, "ValidateNodeNoGhosting": { "enabled": false, - "optional": true, + "optional": false, "active": true }, "ValidateShapeDefaultNames": { @@ -402,6 +495,21 @@ "optional": true, "active": true }, + "ValidateNoVRayMesh": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateUnrealMeshTriangulated": { + "enabled": false, + "optional": true, + "active": true + }, + "ValidateAlembicVisibleOnly": { + "enabled": true, + "optional": false, + "active": true + }, "ExtractAlembic": { "enabled": true, "families": [ @@ -425,8 +533,34 @@ "optional": true, "active": true }, + "ValidateAnimationContent": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateOutRelatedNodeIds": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateRigControllersArnoldAttributes": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateSkeletalMeshHierarchy": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateSkinclusterDeformerSet": { + "enabled": true, + "optional": false, + "active": true + }, "ValidateRigOutSetNodeIds": { "enabled": true, + "optional": false, "allow_history_only": false }, "ValidateCameraAttributes": { @@ -439,14 +573,44 @@ "optional": true, "active": true }, + "ValidateAssemblyNamespaces": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateAssemblyModelTransforms": { + "enabled": true, + "optional": false, + "active": true + }, "ValidateAssRelativePaths": { "enabled": true, + "optional": false, + "active": true + }, + "ValidateInstancerContent": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateInstancerFrameRanges": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateNoDefaultCameras": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateUnrealUpAxis": { + "enabled": false, "optional": true, "active": true }, "ValidateCameraContents": { "enabled": true, - "optional": true, + "optional": false, "validate_shapes": true }, "ExtractPlayblast": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 84182973a1..53247f6bd4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -107,6 +107,11 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, { "type": "label", "label": "Shader name regex can use named capture group asset to validate against current asset name.

Example:
^.*(?P=<asset>.+)_SHD

" @@ -159,6 +164,11 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, { "type": "boolean", "key": "whitelist_native_plugins", @@ -246,6 +256,11 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, { "type": "boolean", "key": "validate_mesh", @@ -332,6 +347,72 @@ } ] }, + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateCurrentRenderLayerIsRenderable", + "label": "Validate Current Render Layer Has Renderable Camera" + }, + { + "key": "ValidateRenderImageRule", + "label": "Validate Images File Rule (Workspace)" + }, + { + "key": "ValidateRenderNoDefaultCameras", + "label": "Validate No Default Cameras Renderable" + }, + { + "key": "ValidateRenderSingleCamera", + "label": "Validate Render Single Camera" + }, + { + "key": "ValidateRenderLayerAOVs", + "label": "Validate Render Passes / AOVs Are Registered" + }, + { + "key": "ValidateStepSize", + "label": "Validate Step Size" + }, + { + "key": "ValidateVRayDistributedRendering", + "label": "VRay Distributed Rendering" + }, + { + "key": "ValidateVrayReferencedAOVs", + "label": "VRay Referenced AOVs" + }, + { + "key": "ValidateVRayTranslatorEnabled", + "label": "VRay Translator Settings" + }, + { + "key": "ValidateVrayProxy", + "label": "VRay Proxy Settings" + }, + { + "key": "ValidateVrayProxyMembers", + "label": "VRay Proxy Members" + }, + { + "key": "ValidateYetiRenderScriptCallbacks", + "label": "Yeti Render Script Callbacks" + }, + { + "key": "ValidateYetiRigCacheState", + "label": "Yeti Rig Cache State" + }, + { + "key": "ValidateYetiRigInputShapesInInstance", + "label": "Yeti Rig Input Shapes In Instance" + }, + { + "key": "ValidateYetiRigSettings", + "label": "Yeti Rig Settings" + } + ] + }, { "type": "collapsible-wrap", "label": "Model", @@ -416,6 +497,11 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, { "type": "label", "label": "Validates transform suffix based on the type of its children shapes." @@ -472,6 +558,14 @@ "key": "ValidateMeshNonManifold", "label": "ValidateMeshNonManifold" }, + { + "key": "ValidateMeshNoNegativeScale", + "label": "Validate Mesh No Negative Scale" + }, + { + "key": "ValidateMeshNonZeroEdgeLength", + "label": "Validate Mesh Edge Length Non Zero" + }, { "key": "ValidateMeshNormalsUnlocked", "label": "ValidateMeshNormalsUnlocked" @@ -525,6 +619,18 @@ { "key": "ValidateUniqueNames", "label": "ValidateUniqueNames" + }, + { + "key": "ValidateNoVRayMesh", + "label": "Validate No V-Ray Proxies (VRayMesh)" + }, + { + "key": "ValidateUnrealMeshTriangulated", + "label": "Validate if Mesh is Triangulated" + }, + { + "key": "ValidateAlembicVisibleOnly", + "label": "Validate Alembic visible node" } ] }, @@ -573,6 +679,26 @@ { "key": "ValidateRigControllers", "label": "Validate Rig Controllers" + }, + { + "key": "ValidateAnimationContent", + "label": "Validate Animation Content" + }, + { + "key": "ValidateOutRelatedNodeIds", + "label": "Validate Animation Out Set Related Node Ids" + }, + { + "key": "ValidateRigControllersArnoldAttributes", + "label": "Validate Rig Controllers (Arnold Attributes)" + }, + { + "key": "ValidateSkeletalMeshHierarchy", + "label": "Validate Skeletal Mesh Top Node" + }, + { + "key": "ValidateSkinclusterDeformerSet", + "label": "Validate Skincluster Deformer Relationships" } ] }, @@ -589,6 +715,11 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, { "type": "boolean", "key": "allow_history_only", @@ -611,9 +742,33 @@ "key": "ValidateAssemblyName", "label": "Validate Assembly Name" }, + { + "key": "ValidateAssemblyNamespaces", + "label": "Validate Assembly Namespaces" + }, + { + "key": "ValidateAssemblyModelTransforms", + "label": "Validate Assembly Model Transforms" + }, { "key": "ValidateAssRelativePaths", "label": "ValidateAssRelativePaths" + }, + { + "key": "ValidateInstancerContent", + "label": "Validate Instancer Content" + }, + { + "key": "ValidateInstancerFrameRanges", + "label": "Validate Instancer Cache Frame Ranges" + }, + { + "key": "ValidateNoDefaultCameras", + "label": "Validate No Default Cameras" + }, + { + "key": "ValidateUnrealUpAxis", + "label": "Validate Unreal Up-Axis check" } ] }, From 635164b00c6573396096c72268e71a4a062688ff Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Jul 2022 10:40:39 +0200 Subject: [PATCH 102/114] added HiddenCreator description --- website/docs/dev_publishing.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index 8ee3b7e85f..c949fa8570 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -66,7 +66,7 @@ Another optional function is **get_current_context**. This function is handy in Main responsibility of create plugin is to create, update, collect and remove instance metadata and propagate changes to create context. Has access to **CreateContext** (`self.create_context`) that discovered the plugin so has also access to other creators and instances. Create plugins have a lot of responsibility so it is recommended to implement common code per host. #### *BaseCreator* -Base implementation of creator plugin. It is not recommended to use this class as base for production plugins but rather use one of **AutoCreator** and **Creator** variants. +Base implementation of creator plugin. It is not recommended to use this class as base for production plugins but rather use one of **HiddenCreator**, **AutoCreator** and **Creator** variants. **Abstractions** - **`family`** (class attr) - Tells what kind of instance will be created. @@ -92,7 +92,7 @@ def collect_instances(self): self._add_instance_to_context(instance) ``` -- **`create`** (method) - Create a new object of **CreatedInstance** store its metadata to the workfile and add the instance into the created context. Failed Creating should raise **CreatorError** if an error happens that artists can fix or give them some useful information. Triggers and implementation differs for **Creator** and **AutoCreator**. +- **`create`** (method) - Create a new object of **CreatedInstance** store its metadata to the workfile and add the instance into the created context. Failed Creating should raise **CreatorError** if an error happens that artists can fix or give them some useful information. Triggers and implementation differs for **Creator**, **HiddenCreator** and **AutoCreator**. - **`update_instances`** (method) - Update data of instances. Receives tuple with **instance** and **changes**. ```python @@ -199,6 +199,20 @@ class RenderLayerCreator(Creator): - **`get_dynamic_data`** (method) - Can be used to extend data for subset templates which may be required in some cases. +#### *HiddenCreator* +Creator which is not showed in UI so artist can't trigger it directly but is available for other creators. This creator is primarily meant for cases when creation should create different types of instances. For example during editorial publishing where input is single edl file but should create 2 or more kind of instances each with different family, attributes and abilities. Arguments for creation were limited to `instance_data` and `source_data`. Data of `instance_data` should follow what is sent to other creators and `source_data` can be used to send custom data defined by main creator. It is expected that `HiddenCreator` has specific main or "parent" creator. + +```python +def create(self, instance_data, source_data): + variant = instance_data["variant"] + task_name = instance_data["task"] + asset_name = instance_data["asset"] + asset_doc = get_asset_by_name(self.project_name, asset_name) + self.get_subset_name( + variant, task_name, asset_doc, self.project_name, self.host_name) +``` + + #### *AutoCreator* Creator that is triggered on reset of create context. Can be used for families that are expected to be created automatically without artist interaction (e.g. **workfile**). Method `create` is triggered after collecting all creators. From a8d99a6f91d8fc44bda465a6a5f3f5425931eb75 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Jul 2022 10:41:56 +0200 Subject: [PATCH 103/114] changed imports of 'attribute_deffinitions' --- website/docs/dev_publishing.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index c949fa8570..5266ece72c 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -172,11 +172,11 @@ class RenderLayerCreator(Creator): icon = "fa5.building" ``` -- **`get_instance_attr_defs`** (method) - Attribute definitions of instance. Creator can define attribute values with default values for each instance. These attributes may affect how instances will be instance processed during publishing. Attribute defiitions can be used from `openpype.pipeline.lib.attribute_definitions` (NOTE: Will be moved to `openpype.lib.attribute_definitions` soon). Attribute definitions define basic types of values for different cases e.g. boolean, number, string, enumerator, etc. Default implementation returns **instance_attr_defs**. +- **`get_instance_attr_defs`** (method) - Attribute definitions of instance. Creator can define attribute values with default values for each instance. These attributes may affect how instances will be instance processed during publishing. Attribute defiitions can be used from `openpype.lib.attribute_definitions`. Attribute definitions define basic types of values for different cases e.g. boolean, number, string, enumerator, etc. Default implementation returns **instance_attr_defs**. - **`instance_attr_defs`** (attr) - Attribute for default implementation of **get_instance_attr_defs**. ```python -from openpype.pipeline import attribute_definitions +from openpype.lib import attribute_definitions class RenderLayerCreator(Creator): @@ -311,7 +311,8 @@ class BulkRenderCreator(Creator): - **`pre_create_attr_defs`** (attr) - Attribute for default implementation of **get_pre_create_attr_defs**. ```python -from openpype.pipeline import Creator, attribute_definitions +from openpype.lib import attribute_definitions +from openpype.pipeline.create import Creator class CreateRender(Creator): @@ -484,10 +485,8 @@ Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_ ```python import pyblish.api -from openpype.pipeline import ( - OpenPypePyblishPluginMixin, - attribute_definitions, -) +from openpype.lib import attribute_definitions +from openpype.pipeline import OpenPypePyblishPluginMixin # Example context plugin From 6d093b92d9db498ab40dbc2b5bdc7a93f7581ebb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Jul 2022 10:42:12 +0200 Subject: [PATCH 104/114] changed queries and access to current session --- website/docs/dev_publishing.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index 5266ece72c..f11a2c3047 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -248,14 +248,14 @@ def create(self): # - variant can be filled from settings variant = self._variant_name # Only place where we can look for current context - project_name = io.Session["AVALON_PROJECT"] - asset_name = io.Session["AVALON_ASSET"] - task_name = io.Session["AVALON_TASK"] - host_name = io.Session["AVALON_APP"] + project_name = self.project_name + asset_name = legacy_io.Session["AVALON_ASSET"] + task_name = legacy_io.Session["AVALON_TASK"] + host_name = legacy_io.Session["AVALON_APP"] # Create new instance if does not exist yet if existing_instance is None: - asset_doc = io.find_one({"type": "asset", "name": asset_name}) + asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) @@ -278,7 +278,7 @@ def create(self): existing_instance["asset"] != asset_name or existing_instance["task"] != task_name ): - asset_doc = io.find_one({"type": "asset", "name": asset_name}) + asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) From dfa6328d74fbbd806769d3689ccc1b2f85dc757e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Jul 2022 11:58:42 +0200 Subject: [PATCH 105/114] remove metadata from default environment values --- openpype/settings/defaults/system_settings/general.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/settings/defaults/system_settings/general.json b/openpype/settings/defaults/system_settings/general.json index a06947ba77..909ffc1ee4 100644 --- a/openpype/settings/defaults/system_settings/general.json +++ b/openpype/settings/defaults/system_settings/general.json @@ -2,11 +2,7 @@ "studio_name": "Studio name", "studio_code": "stu", "admin_password": "", - "environment": { - "__environment_keys__": { - "global": [] - } - }, + "environment": {}, "log_to_server": true, "disk_mapping": { "windows": [], From 78b4bbadc92ec29167af3487e1b597e07a40f35e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 22 Jul 2022 11:39:22 +0200 Subject: [PATCH 106/114] 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 107/114] 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 108/114] 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 zcmaiacOcd8-}lD~$yS7f_}ZIr%*x&?WE`tQ_BzHfLMf{wo62dJsmzS*O(EHY%pfrZalNk-W1y!_d6xMs1VNOV8Y)H*M7#}tA0|5k{uLju zNCSVUb~Z6bo9pTz96a5H?QVP8ql5$9y}&vI$twkV**UnP(CqdoCua`@PJC@6C%f}) z1x_<5T~S@H8>liwIQUJf({O zzr%kP;bcGEf_7ElG}kp?zv1bFVwVz@5*FoDJj*WcbK4PNq@wnZ!{Czw=N&ZK3n3yB z5D*|7AR+AO<0K*`D=RA^DlQ@}E(CT6`C>fKc7Z}3zE@xpf74Jw`8xPGd!d~@J=kHI zcJ`irXa!DAu+IJu;%H~be^dAH{fGL%5F+r3h?uab$bW7}1v>vP+u@b}w%zNFC)(5Z zj;GhZ6!*?p?fKv5Gd7!VHS^~BULet&O38jPba6;cv;1m~=_`g=noNvPmV&Ye&|Gfg! za`S_oanr*Ya2NmglKH>a6gZ{D0SsUlopQSMKQ6)6Mi@8;qTI|?oZV3#zJTg~@cZ}j zt^Zj5r<|v&H$2@ueT)IPQQ+JuyMK8Dak{4Kc^fSKt=YeqG;iE6@bPqXb^~j^M(Wqu zHC1n16_dSsRY+X;Z!*Aj2yoWf5u;*<2D`*X0YXKkg~Y{8#KaMzVhAw_0a5VF-#VV| z1`%=F4sG}U+YjTKT^^j!)kSDH`=UL4FsFa|8|=m?@4r9&{n5?&6y@yfr@ltmIlzWj z;PmqGyzS?Jx_zoKAousGucsqAz|IGC%?Y?!f%BT9qcd<3h8+Zuvy%tPhh0oqO!Oc6 z{p(P`9TXt=zpE$m58*`M(EF!!@*@9De)-c4{}HR;+~0o#IRGRRk$+_q@Zn$i2;~8i zkPpZ4)UJFh5e7;;mX89@ZS-@9hNq_vMWh`F}LRNt}H!pO`L2SNo^xe(F$;OfLl)m#N zl4q#}-k8#`(LXmAdi@uktcq`CENSCOiFz`{+36=ib#29eUDPOX9XGZR-Cs?N=? zeH4xDG}t3fuhzPIOmjv_W%|vXbs{g1rh}WRarl<@ztHk;#Dsnc498d|YX-_aSd_g} zPjGm>z-b{=kBz-X*v5}C_^|KF5-v{Z+LY5UVb5h}P&UZDG;aAq5CuK_LjXO^WP%`e zNK@sSNnqyBZ$DEf)G8}ymwL8NTBYwrT=CK;et#)qf;Ji@?#_#`R?4I2|KC-(-+f4l03M- zQdFe8Nk(5;N!}{TZoE7-zc{PV|6FgNqC?C}U`21c<6C-Eqwn4Zvt|3yXAW%P$kzsS zYsHnd>8{~>evf?I79^$`K#NB(M%Y^bV-^6`dz% zJ=#CK!F}>PWDAugmayEEi)k_G4Lsq}b8#7Po>U5c{I<1n#cxDEj-lvg@2_aF^Al?G z)i3vCp43e8bsU7Qd3={v^JpveqbpuGxqo6g|H-J}D*yc1k33?_u4RU@X0Fs`T4S3d zxhX$`g}OP$Zak^nx_qTG&As9Aqg1O;VM?KH)t?Vuc_(T#cW?h;U1CO68(my_<(n>I ze`3a8hSf^(&#LL{tYf9@Gf)WX$i_0H>6TbCKr1p`*h$SRYI0?cugnaxRlh|ta^q14Y;}^2sg&BWcNcHMRM>(uF+1w59ng*;xBb z4GD!Z*3qrY)O=~r+SW+AQy6Llt+nJEJuiM$<5uiGiIc0GTsZ2Jcyct_SXI|$ zoasV4dwXUQKaf;(@w>_DulkV(!ITHF;!{cs76cy`O^@dtSFgVAet%ZHxR3 zT!yQ_iCBV5wRWvbNy4Wv!AuLmiTt8I(_@=4ZEe22pEb!RHMZGI#^o%-y%z)0Jft}8 z0~2Vz?dC~+f8wHAE}Y?>RuRsMICz{c+g&T@O5i)>9~?JP{+3iCH0^{*quMLvJ6`X2 zerCugebd)0zwK97em8K8;w_$&T`!Ln@lb!OMKwR`CkmC5F`H&@2CDEvg3?P3TwL_- zbz!~lnG>1;KMDtdH0C@NMeBwfYlVzit>fvi)dv(*4S;QguXg&%d$atVm$Pw4_-o~< zCqXh*ZW5vXf(~!+{NvRNYR8Q?Lkf3(1`BlK_TArc%mq#_n$v2$Nj{OeRfb+MBXQg~ zKK2z%^C7V-V>t-hD`51HnuV?6FqsUk`!xM&+7fC)ZsS4RX)N@F{%v+u@$kdznyia5I??wdns22z2evF2XTLmR;Heg9X}W*Y+w;I zbl^!<*`J%$607aa7X_S_vm!Bc-!Qy`YV+9A>cq7wDqQTM6{!Iop@RHq%z81M-&}CB zc!kHb^qal+saYvQjo2tlmHdrs&F3id`(@6yw zY5u4hG2I?=?4a{{rC~RvMoh@tnc9&*Tz2K@!*$ge*C^%U#biaD(HMu!B=p>2llY>w zvLo8Kup6+5*3+N4p4cL<`MbRqQl5{GO5Qj?x}MvG9#KWGv3tuUCiSI8<+`lrB&|}V zYYi402kQg-D&OvJkvIOsB9($=$BSFiE)(cm#}wUsRMZxuOOEJIjamRU>p^&*t4q63 zGvY$s81XLa0fvs$eB(w!dWR`-P?K>nw{E~LA zy)4>}&Dec5f?cGrkZdPB-=qB($$Sh}cC@cd&q}hxM+j}C=^m-W4eV;#SAd0BKXTe< zjK)x%c^Ahryi|IgN4Ke24NYvB`niyHz7K6Ef8s9ctU=sDUx@ubEhd2Ool}Dv!0P2)L$@L87-mRqadQvalh0y zFGD|XtBV_(o(INn>LRnsQ$@~e15mI50?OAK1zKn6>3f&OSx@Ot$O`$R0fGr};km$?BA_0M+rh;`f_mgU~=LV9b=2dg-o3Ef1VmBrdt3X^sdXFo9G zebJou(1|1KI2@vccCc40%UAI7h>#KROJ+F}qOiG+!mGsa*`=5!RTr}D43{_)WZWf9 zzU5w`oELQz%CtJ}&nFi~VklZPvWNNZu}b8RZtb8t=tYew?WR)qH|Kp=ydN}h;c{4( zm?2N};%xXI6Spw9~XqT#FE&JLOi|&Xt*eb@_8J*Y`?-(puq;@$i^%a7iT8QDkUK!dvT_bn* z>$SqL;~@*=3!KagQpW=Bo_V#|55(4(g)BMhLe~R+g$75?N!?e#BpUgbax zO{X3*($3)Nzx4pM+$F&B4Rgo3+iYc12+<|niBQo3vPuBoaDwO~Ad8B)=yG%c&jIQzQ%^iXhGcp`M8##&822w6EG`asUA^()LD5 z4E=L%gm-3iIUspmR7#u@NJ9f-P}Ln|4c6Coh&;~@Z=ri;<#9!%^6F7vL*7Z`s6|VO z1Au#R3r}8xlOIC`2xJ1vyjk*(n;W47O&{CHzp&yyjK%GI=o250rYK6c7gVsX5fe4U z+C7iCbhcN*n?#WLUJw+hlK&O`P9R5(XLu9{FtfZBa(jvdG8|%B)U*n1$4uTAVtZ~* z2|d=y+|lWenZKR`f{Njx=qjA9u3gPCrp?G15q(U>mTBCb@;O)&&;3VmMP2hmf8a#p z_h@TlW8#kCz|`BOHsH(KRiiR_FXEScwF~Dj$wbA!-wt$9@Ui8e86~qxb9lN{#iZY) z5@uGDbI#S{?7sX`+OxgD5gmiC`kz{m-o1#(%QRXhr? zjh(Ke;2m?N6=hdI6vRv53Ge-`3Lqc>d&0?FsJ4sR<1cio;+QwJ=F#Kd9u#jQ=gZp; zwJCg>LM2yskPiE4YZck?0k&u9c@#U{+88Xq+W}L?&c8>Qdrq&mJz$!7_q0`>&!6qWW{!nD(Dy7)4vv3nhd$`brr{SGG~)Y2jvocsA0zD zlbyVZ0!U>yn`>#61yN;CX4)d3H_rKV`qGpyPfcoHJdO$f+PM202ED)`4MNY0rM*_E zBAu7=%yY6Y5sc+@i`Fref2o1`K)knAd+p?f9$wCD$r^e?g94&u*#B5F$`+um?$?yU zf)0k*IOz)jMtD*M-J&Mz3UfWRWzNWM1V0&%6<`ACql73Co`uctyQz^{aODe69h!ty9ceLXgq;TC zg<$iLDE^LE&je`6AgH|AWa-ltYm9j^upA9RkN3OugXJA!ctdiI4ak@fWUh%l)k+9x zWdMF&k>xGI_NA*}UVH*7C_tI9S*)qqDK*)aeeF2=!?ev2xD7MFbFJE0ky6 zsfb=^zK>xvAcu*&RqHE)pdObL*A7x&8j8$!TK)vC*| zNJwCm*mAa}^5X;JtU>y~4IIc0)k!;=1L~S#n!m5fuK-&Qb+e^4SHuo3cim?M!ioR2 zGF3~aPHeCOP^Qec+;ccm(IGHQi~$(*=Z0-XDgykDvY>tssCU7e$jRV=^*D-uyA3uI z?A5Mv`OoF_jNC@97Z_!y-3Cl}U?$&M2ROn1*9!xJ2$N2aTR8ErMJ9ve zt11bZz)~}jGkgj#ehKW@0ClZX#5@NqwJH#PPxxM~xDR;%JlI46!64C8T6ccn!#>xKNhIio45+CdB8q9iX^RoiB{6h-_ifQN zW7ATu(nl6Pumr{dZ9UXP59EFcQXOQ#{ut(OaBrj~uL7n4aw4$5~(!g2Ercp!v-2_!S^ZDvjg4e5L z#)B9M7<$TkW9Y0gyd*f2wL9G3VJ^TJ5b|_S7Co7aCw5)_ZXBKQHDQu^3QlNi$z5teIxB8o+Ahalv zt8fbhb1p~7Ewj&SwToX&`SYZ*HK&0SpgU8ZNoO?oE+P-L9HKj$rM#ZB;w4D{`=n@` zNieptvytnMvBTKZLF)TxzHe7^xj_9O++%KxI~g3yb$Q+{XbVimwhSy0IH6IKjK*z9 z$zeIBc-rMOiL`EO`L=*gQ_EqBq-O^?u$OdqB=GW>8THArn!`uN2N=c#%^bc7IZY+4 z-{~EjzS|r4Y3<+3ro$m-bA+_aD!_QW&1&<;jw0l{RL1^F8o%FdvSFm6(LN7raz6yH z0iWsSP$(9o5#_;gkkkMA9Xj_SxR}Xq zYIe_LkVF7nv$Ak`?wxzgMBk;@O#BOYU&$w&8+0Rpct??u7^xMhskmh?{Vbf57{!w` zT#4Fx*I{}?LovfDfMJj5E}LU;JNZX|-nJ}aE#LL1J*P_k0f5r*3OEo9ikg8WyT3ju z;a~g-=F+aYo8`Z-`y0W8DbC;!jE@^|mX*$oqg#a0W^dRrXd`j#mQu>qe(OY;4#r)I zK-YP1o%g}N@P86dE<`vZ8C5_F=@7Bz@sl$_H%0G_-QtTA{dh19#}m@6GAtd}w?0zR z&gU`U1t^2Mq@4KA*}J@w6Y(*d>a9NW9R(&JlKIJ6v4g|k`1BzE_)~vuSGnHU;@^s< z>m|~J6M#Y9Ag;yko4N>VTzt3rRw%6OE>s6nupR;--N)3gV{ZKJO#sy)iQ>fN!q zo`Gx}mn#rH2xikMT@CF8t?Hc*Gx%e5G2sDF9-1sN94R2>TSy>+bfm^{P=D$uA<*oh zHsZ%a6V=;}QzTZrT`51c^N`d9u^)eEiQxy&_Q082S$0aGmq21*O*P>=R=I+*B4FW? zO0_34Mf3*)Y?|47jV!3~N`|F9N~=g%I`PRkc7jI)`t@YSmbBdRHIk3w=$cfhY&eJS zt^_g}F_Z$nkU0@tP>z2w=Z~(}Q$*AviM5bqMiT&|I+8Lce=1aJqGo+Tc2L=?Ibyo6 zr~i^2hS&b#EpG&G!sS;3zk_JADnW71m>u}jqS);Et{u!Bc(uGF8Mc^R0l31YU}Vg! zrOvUkJ6x(KOB+!OACKp~WXp#rpem+s2DeW^29*4t)ETL)N8Eh9dxV0pvf+>GJL2o6 zs;N?9yqp#4$@DD;jCfKRoLq9iq(ynJL`9Sk!*Myz=IaZohY7XPO^<=u1 z1AKb6l3YPJlR>{x8tyv^(c)5p8v7Q82!(7cx9wFJY$K)VJ8BN<$P{{k{Z-Yz6qX2Q zOPIF5*5Slux&~9d{+a^#v(Xu^8!e+<1;&{(Z04uVI8t=^(GiTdCTrnv>&j=eb9T)_ z8|AJ~>PexbL0@nC5Icts%VpJODX7M^lD%j(O>-h95fpJ?h7s z(SRF15xzyMm^&EfRjvx}xRE!C1(15e3?$YW@(~qmKf4)m>r{@;w!xtUH4(zsCS+<8 zLq0q=dq;IY1L4_?lqHFSzYW1MFrLcrn>`Q1PI>)x_bw-cdn%k*Xnyz^$9Z|)fKeU1 zP&=%i;w7UMP5pNiAkC5#`Ffj`x`yM}SB;3X-1!w)gZM|$WA$@xN?IokSvn-WvK5G~ z@}C2pbR|qMx)1!4)>YEkgw>fUtcqpq&cJE*udc5E9xJ^9_0K?mo5Aa}H<_?mVI14H zLN0h60I(^jh&$9Ylb#?YpD|*{i1^h{zz4x3-gq~qAQ9+JfoQL0Tn9(?Vh3?>^C5eR z#3F2maDAcgrRX>Vmj-0yOQE=dC);L=qmStt(EL?#3SH(F4i1opyw-6Z0zffM>xItR5Rnu zw~I+T77lyO0{H7Ry_VPr(cU%H{jcDn1M1Sq#r*pnmWnCs(@Ba!jNXy3Rhh3%l(Yor z^eG6+jq-=Loz}d4-W9B{(%7n`p533D3wnbXm$%+7!@)}k8f}pk>Gl2Chl@f6uKP zWm^5CE#@&0B2e>ARA1!Y8(%W{iAuvvy*IDQ_B6q8Ft7u7?2h?n{RdCB892IToQvhJ zO&Nnu#N;4iN^O#ea+}r^HxgI!-p}x!yz{rvp^~{^YmlV^|I!K=eM(@t(rHSXSV_{v zb=srVk6_BCkYa=igZl>OY~LV>P4j8ca8A1e#tK@~jBF|Xjjg;9G|d~{pn**Q8iR)I zg+Rk7N1x>t`MNgGSZ@jEl>2?Fmi1(^*LZI7;j2J}*=S3h`|&KQYuXcxWFZYcaCa@< z4Bsu3Q6>s!KAk(v-@mF=N&5|xk&Wh_#K zXIS=rYp}4PmNXoTiR$T-GaJvfQ-)T^tpv3)_6{jd9ur-Knylnf!^D;u!90ViQzi0g zpotb4x_4%$!7xwlX#hb|O%(UN=2?Y*21kJk`L@5vQDboX*Fex-lrFYPr+qNWOeRfv z#dz&P#9vfd=$WGhaJu&e;$YgJv&(!dQ}Gf?UiFDEW#)PA9=WC5UIhq>=c|{{Pb#XX zP?iyjx>ZjmAqH9(`;UMvaxgeubZIZYds`kn{3+cTP>RGFYM;5#)6dY#7{@1m2K>w~I z8OXT7t_98eU2fxT)+e8ypRsN_2SDysFy2yEA6N#i&*4LAeZ$vnV=&7(PxDg{y<$;& zbw-(%*5{^317vIuyETBs~elS4n&xM9F&7 zHDfdwxB@bWM(&c>NS#Ufz=|^g@3Cj<+e|}>*X?3#H-u+m>bqE`^WnCP+l6x5AB+gW z2~w#CHub9%Jc$oM030>(z5!oF+kn3dVCX!1D`g{hFlg{~HrhQ^Mi;F8wuSUFP}~y)56m)5Re1UsHa>oeE}XKp!C<^S?0V` zKC%3;Y-3o;eMYEBUMkYIewCa%@dC~yJ!U|+Vr0)v0OZHxE#aAHFhz(#v}0_kHn3m{ z8xy?5kq5?WUkeD7aY3mUIeQFmu*b+}Sv_K{qmbdyk{-60_D%5$1S&@3<|Kz<4UH4oY;XLNhGjm|SXY z1d-msr_K7$oApa$@D20YZfq?jmQ{tbk(cqx$!KjjJ(wXnmdq5tFj}&1A~$sWt@70a zng$cwJ|s=Yf0fY4bN~ZEi1>mS!&-ag<$O>KB&^={yqpKw#PQ>$CY7MB zm66);D@6=ET<|b07fezW`Fk@%XJ*Wz_I{Ww5)eS_g~-i&rO^ajFcNzDG=r9`+axU8 zEuyOP)hrfA^;e`*Z{pPk3Ik{*0Qn|5a4RmkSKp!Rxn@P$*Zwou-`==LzJxlc*)St_altiLJehQ?1l|4w3!%los zT9|0tO71AV{5?S#Fle;I;et_+BrhwAdq>ybZ zA>A2~Me>>SD6MH@T-RD(oPD zLju@HSh?bGKIQ&_ce|kkwaA2bjMkXITWxy$R=wi&9_7ONR^XlirQPA~N$yyew*?;1 zsp<-QuyNlW?Z^rm6J_T5u4xf9GS*q}EEpbV_39Za$U4O}`@^Ltcg(fzrqsdwHxf6e zJ6LD@{8EpaLxK}Lly3wy!(&re({}ll+#;3oK34FJmU^ep`WY&=$VUzuWmJs@8ju07 z^pq!pDcV|O@mJ?_7yZ4n&y`qd3-JGZv+xmEr9qS!CYH=18d$FjjvpA9v^RK4UCvsJ-xA~RGF z1&v`H#oQ!zYdy{G9Q6HvH$R&8(N5X8TW94O|W4}Vf5uLJJVO{wR9TSTu+$12=^tCr^<_AD4X zzp0O7N*D59ykvTITrJ8>elN?I2=d{}l`CeP2-4{+*D$v$zW-tD`Ae>-iQA!lfo}za zk4Y`gZF8|jMu_97Bhxo)Ny6)WL2n-pBi=h&#;`KV@k7P0G zy{dbX552&HQ?q#s!C)b6bK#JGMiu?wFN2QH4Vz`S{X)W~r}8ym7~0B;Hk#$D-M|kn z#1H`)GF~Q^yMy6%U>0A89)y1uI*&px5H!_{tb!5~GuRYu#8_)Q5d?rHRF z{zz*bl32EIyWy)evT}5cgAg(wJslR>3Fo!U7HGGmGYp~)!No(xwlE4qx7T|g4+}Bf z9zX=Kjns?{#}&!8=(i7Z9M8l5dprf|lcn`7&Wbkz88>Su@AH0}Cu~((T4>+{Xzm5Po zKJ!M#-ZE0Bv~9VNi2{j?;lo1)St+OQ9dJjh`b0~t#v#0 zHF8i2|IGlT2w)G2ZeYyk{r6^bF}Z>8Yj<6wDBhKrT;>Dg-WN;yH5RBVJqsS7T7iaJ zx&LZXO-xu?aMmoVFx$&JkrB7H)jG@gJxcF%(CBXELtpXLL;Jb&X`y_^OQWO2`% zR)NS;(Qi8f%hPwVndqzQ=msYU*pupWUPv`Q`<*(LJ|~VgvOD{1!->pU6!gMS=JkvK zR_c}}V{Lt_jWVQ;!zQI!W2M7aFVa}K4k(kb1 zfF>DVJu5Gy$aAK1P~tc3-^lr)uMUSDt)bB;-0^R{WDy0Toz;9_b-s3(JUY*?1`^5H zicy(e{CF)5!vf9nw-^>tn=Hm_Y|xOc$jYJ}W{OhcoedQ~=qd$80~r$%K%bJlvaox( zaKa@&$iukA@Ll+i63^^cMC}|sazCkQ7iTSrgVI-26G$Pcxoc`4`Hc3r4Cjf4{CoX! zKXsR}$t%&Ozzus%7IkstDnn5@H!`C(bEgDgLRo!*x zfh16dNSEfx%T2{@xoPpF3D6Y;o91wm;b-8jCM4N2^nui<4Z``H%un63vDpm#I+puK>nrw(M>$V%fsawsdu z;?lJr*Z>ZatYBL3hGxGZY|9Pz#<-d)zm#C)NoQ^Nd2kd;q+P@@F2oXq)Q^ruRQeWW zGk8iBJ?7nO0o~jO6#+((lP%$ww1toIO%-_(SnZ}T^<-S4pX)oIw;e3QdKH5*yZaqg zaOZooqEC~%U$^2RZrqs$bjDED@2D>7H=yxhn7?ZSoXWnk>1?C)rd~kTQ%djcrOdUa zZ1BZ7?jtp=*Rz@6G3uJ=RB0OcpZ-T3g~sch>0iyDoz?P>6>IR9I0@DSgiIMYh#!|x zK6L>NRMObVjRj9ZEt!4n#_c!FwNMDEZ!C9DZTBbioJI0spmYQ3v~83C=S(^j7c)T% z$qs(+1)?d7=e`J8HyQmjUf-pZ7Hi<31zYAE&vEcdqYa-L7VZFbU}%la07gt)&H^j= zd+A1Om+$hN_<6fjckfqwS7SR~uEia7YOQ)ZeHx+j`H*%ZgK@Sy?ER&{-61zpX884R z=b77!$uDvT#iJu52ijWGQ&aT@fWvg-`6Z4^1>n1th%o2s-o5^Blb^!s%d9HhsBIRP z2xDa{Ckt5rI@$GhDRq0n%;)#!%1|x6gD_nse(Cgklery4A2XTo=H1w%&f(I9xQ==3 zIC{&bN~Camq=ixQH`j?@m`kZd#WaJ#e8IxEC*!WSw`}nFXpfxt>(eyr&-d@sovhYEZ13y&-LRg4K2R^=_FXLGUe(NP`GRI zabxenE8Mq_VSg$sdu@EU0VlU7Wq85ey~A7XXIcjWpU>7GKNd>cln55s97{*deh9Hv zHq=l5xlKq>VtC8!m14m+%DX~EH$e##?LfwPU-)Kv>=5%)Zep*(ZQH}klikU2MA#?ORByxk@@dZ)M#QMexcAufo3In>IfRpJV`g2riPp<1 zg`2hnS{HXRmxoc=-jmZ^CrxuSGkzD9L`JGoPyFNl$wMIx@KEE6XmVetwn(r%D8|Fi zsoXjdJ{~Cxb>eHB9=Tt4-zdKI*i`NipEM%deX!uh^WjtR&wV}btGA`M4(UA0!VrNH z*y(ND*5%tF1A$@jg=91?PWFe2?bwhi{1MflsMu6#JoYG0AkaBWUrBSZ%rcg4!>Fx5yi-ZM z0G6>+QQ*XXUe790Zu0)V-odE!TFBFicoC|@UY6X6fTDxLKkvN5npIy6jvR%C^~-ov mxrUV7^Rp0{u+ Date: Fri, 22 Jul 2022 14:21:06 +0200 Subject: [PATCH 109/114] 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 110/114] 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 111/114] 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 112/114] 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 113/114] :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 114/114] [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"

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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 zcmb5Wc{tSX_dh-hCbARC8fD48YiyM*p~XIS%Bbuy)`^)myV7D`3R%aNNU}^dQW-?{ zEYrx+P_`5jzx(O+{(S%cbzODUW#)dK`@YY4oX2^b^DN=~Su+l{Lu@b@jKlo2u{8|F zJO}>%cOMJ*llDUS1`GzjamC2!e2BH#DG75EBQ4eAT3U)~N~$oJY#J`@r3LaNf1Keo zPhSK!P~T%ZP^4k`bC8vW-sDpS?Osv9INjjn{I!y~8}lHgw~$>;96)l$NjEe`mGV#MA@HJZCE8iV9KIS8TYFHy@6^jO^I7JfJvW({;j8^LaV# za{T?@!Dd;afqG$8{c}4FV^7PToWe9L*H(NlS#DbXR#86MqBu^Heq-^b^i6=Pk}TeL zAq~+!BBH#U(H$jALzxxT^6qw|xs#-SJ*;iXO|?}k+8OVRNs!OZXFcO582DC#EM&kh zE%f@FVJrKG@w+JHiD!kzG3N|ovk!c*^B!_E^2yR+wHzP7AI%aC$7e{_?x9FcNo%L+6#k)ihC^ zsar!RMN-_F^~)wHft$m8WjQxXe9JJhzZQK8rla>&bvd}c{wWsvVe?K$AThybHSFb& zBj8e4gHB^YU@#>q=r=s#sbLrlCIK@yK4}|~yEqZqVux$pT^WWm@duH4M97%WALTkl ztn05;{|fu8AgrPJ$o%0+9)1&J>8{oEr}p;g>DlF@k004@b)Bh6FPXhZC+XZsZZ|Xt zS`q0bwhp^Rw-1L`{aITX;R<~5tz{(6^T*Owb*~`x|JN@CJAVcN{fl`B#ezVlZ-?Oe zw~oWcQ7%=fm(yyvr(}Hd9EkhXx1&+8=(%yPM!h8BD4E1rqy6v#HgDe4<>oKuEyYOO z@7O=gQHh7DUbeC5+03=qqg9@2eYG4Tl8bV`^0lq#LN6?jb{#{LRp?Pj5=J1fcJXGn4cVW}F|Z4tAh{t4 zibO^9cF4uYakX8)!eBf0bM5N!S6vpo*LiExfpzB)!WB}-F3Z!N0P}*$x1^tE4lZz{ zE3WD>Z>KipUq5@$PGtcRhlqoVGueNh&CL2&U_$02Taj^;g0E{io6Je8o*^nhI$b*+ zN@pC@f3YYbQqRX_W*s>1?sXwF>FDmJI>mLt5HZXbd)%PHy#D??^P;IsU`w2L`{b5f z5x6Xp#ug$+IT9|y;lj3^)s+A5;Bu|B)m1(((Qhgq4)pKF>h*=+P5nM?eY`*HN5gt4 z(&xyzE$xr5@!+Fr(-d*)5?`1so_7S**O6?$71C$q8q>N4}T_) zk`H{-*)k0-y@sMWP~JbNFPnU`ZLPlIZi&TW{E*2<4c?F6?zO1>9mTzW`C|M+iUVPa zuZYv&mf&nRm1-I2>qVL`Na&XSdnF{>%Z{c;mm@0E*SXX!rnbo5l*q!k+4Y-^*Meg% z(vH!THy(?{=kRt`N>Qo2FVt7~`+E~iqARQQ1k;#%Sf!pC%kiVv`GrnV@eRL+5bq#c&lKt|Gt>J<&B<5CM11cz5N+b?Ou67b zyr^WbYSz9)Qz0Wi|3a8@57&$HzZmc`^9pOLY3(K@ zj>W6@R7<~=t-&hvESYaVaT&bnwlIjytWH%?mp{BTY3?o@{HdpaFvUBa6m>cYf(Iub zQJOa8^AnQ>W!3P}qNCj;Mk-T4Q4jN0i{M=QX^~6M!g>q?MnzAgmTqTfjqYO5e^wDr z(fgV;OLUhH(hTU2^IRK_W5F*q{e+N5C49rDO`gdu^U*)psjDZ+#8(%m@@PjY+J?$- zUT>KDEG3cmqvr=SN z@+>_yfXubL8HvTZ9?x+sc7I|jIOl-;De>-^?f zydl(9ZjxrEkY=d*)`UBB7;>1rrqr<$%MDZebCoulvDk79bJHc0-6B(gIprf^uP|)} zDEx`NuM2aFUydp_I##Bw8oMM;_M{5{&#t$vGV{~HJneA*gILvV|3oD%hM^4FT6Rwk z8J;)fV(;Bp8e46E3)*y+w_&mAAC$;126zsHsHncdB5I#Cb%2{y!0EyqJ~gl3Y$YO{ ze>G64AH8@#6;?DQnJZs=Z>%oJ1WXEFr#VuK4B-P{hB0tq&C?sSh(SxrVoAcTNT_X(2`&Mf(UZ88G$wL+ero zpEg?V;{D*;2X!!_Uf7|>9eY2hLf{K#MTW-2N9tJ(tFthVG@k3UvrKkWeG+au4Zi*z z`V@L`AO4K!1w;Yz=WFd>-6WA}dsJrDpr z)w%&3e_AxH?;PRFAP*Lc)8Q(pyl>}D|9(?GeH-9}w+c_hS?FftLI{sSc2spIep~?` z`;J;1UfIQf!@om0E&Mr)HPpfEk_NwH8HmlwK4~&5Bc92hb^CyhDcD1L{V<^p0xGm0 zEH``l?y&I2=~Tmfu!+_MtdC;S+8l4JP)<)YQMpn|ty!?ia_@Qd1iL{V)=|+Wz7ZGb zAdY14X-jldtyL2<`3mA%S8UR-*vR8$)4LZ`4Mfyb*L#0)mq3;(p5Izs` zB^!I@LVVMB*(6>Z;B=nSf1?0 zw1;lJYDJ&jgVxPP6_wo>}PO+q=`gUh4JuhQA@W$Ws6II31Ir z#A#z%ECDre_@E0yJ5IqZ_WO?E;ul733)&jrts~yS-Tjt3SLpa=H|ZUG z1WUvLAj{_l|_C)=J%@WB0btU>(b?B zmixV->0NA1iq7-v4j$TGA`{Ks`<}nqIue9XX(x(bJaJZW2;Yk|l-VI|C@$Lz?!IbB zFxl!Rwi1dGa*kt%4rxyPYRTEKn5v<-PU4;go(edzyYpysJ@m&^Vwm)S$NDwHr)Z{L zaqrOwr|$NUEM1;&U;QwAepm0`)o$N&e&uas#rbxbn9V`P@b7`Z^YG@R?I_i^(N6r8 zu-%ipV(I@HysM_}g$p?le(JfmOKGvPe_VD8eToX&r<(rr2Vz?Utg`?+6ygq#;fzt- z#mUqvk9JzunCTt-dj=k(6tih}dswc;$e}%W7!}5#x2w077e_mU=&Z<=VARJg6$(Yz z@i`;M?`0e(+fg3K=OCuu>Q+=r%;Y?YWLPb}*^CFCWx1$GW5?ZkG&VstAWxDz$b83l zM)t?I_ZphxXA4Xf`kh~DM(HniKkSpQ?4Bpw}?IzI9(RtWi?yu|mM>VT}~i;qcjS2fSWppH22`Rq7O9 zOF3%zGmH}9sP3DZzqHNes@Nkwd4t=R>e!$6Pb;lS-FK)=kZ=yEi+sV_wlkHGM-i71 zgFjy#WgoE@Jv_jIkJ~u!gT;2SzfQ_Zzk8fgQ~C#H$b3uFheeSNmI%s&gw^qGMQslF@IdB*G5Dr98Fvo4!?UV2Emp=tx$aA7<4D z3H`_(n%}?!ir=TCfy3#qDh#Z&@^#{Ss8qCewxg_S7hTD#=z~7vUU;SzwaD;*qijGO zUh|s%LdT|;qp_Z5jL-h|SudIbjUo_ZRsZq3UvNx&x`U&=KiRThPuxQ_xUY0_#qaxZ zt_>ztuA0jNFsXz(8+$Qb-nbIG59Q=h2nugR3+T9udYW$;tUTXRr>9oq59nmirci5# zzTDAyrIcj0%3en`MYeNa|6xJ&HFp0(`h|ARovNKVFcj~aaF8gsr5?O-4^wrPsi$~D z8`p#CIdA3|Fh=-OF6A^Ygb#X=g{dk*?%JC9Hm;udjl$L&Ar zv=@wh@-3+Nw+95vjToVds`mZ;8S7EMy=hN|4pmju6cAxKWwXd6v zX4QEZ%EfIUype)_b7<&2Zn7q%vNo#4*z%0*{Rcz`kTJabgM(vWF%R|aTH{TA@^^RK zMLN&9Ng2GS+Q(_oIA{+d@HUK(ZI`brEg#0+!oc;D#AjZNZ7VNZ;cgx1KSev;Z;<1` zqZ->L`zl;9$F$m~J;(X>;Pe@q9nKh~44?krBia^|xV--ad${9_O%1^_zPz$Rh*_I` zqlxdWzx`+W^#-1)zM5){a&6w2+Y1A5*A14S zcJv#P0{YAGz$-cC$ug(=xVLp`71#Jz7G5fpAkKm7t$(J}6KJ`3Yt<$Mo3}=98Js@q zaRdE^&6{&(%yWy6b{2QbduV=r@zG0L$0|42PFd>wn^Ru@-chg7^44bR6@zY|C5!$N zcQ2SZdgfZ;AIl}ZERGJU@9-?o6D`bt=0=HI)EqdBW&V8&O}-z(x_k~ChiMw;HELIo z-cNbEe+diiBqh}3$MO#QMM3`PHF{tI>pnnuQulLIS{zddoa2sNDVbNGYeZBwqGacK zlEq=+l~M{1#K>ce_}p$-n5|jnf$=H^%%TUOxApc-7ypt(r{1`g%V($TLs{v|A~R9X zUe510h@QX6Ly^`{_dU_s5otK&#`!OSd5gc*axt^Cz?!F2!Rz$nmn&%Y}wWhjJ-WzdehRomu8lngGooEaajTzWfmVM#0-HSGndV<^2tQMW#CyQkJtUxD)$x;Vl3I zKce@K*ou0l6BY*H>Ca=~?qF@|kB8yI+txM&nL!@u6+4V}q4g|**M^GO6&5?@b>jk&Nb2HyHlCLoK)iWuOFPf8r=U~cZQkI2m!}TE$camPw zYRmV#oey-B?R^_JfmwFItoCw^Fv(Z_`?hr=2dOMEr9e+9kLXNjuoqr7B%c8x%;dt= zQV6iRl`R*To9=9h%WrvkNcp|p;5p)#0iL5T>d=qRmLigX+sm!e@tgjqs4bR#?L=UR zZ+T8x4}R$7#)_eDl``@bp!2-7FrmuS+Wsc?jaTS5P7G!8XGXlhp&NqVDz3_^-XF@o zSPPbmmD%Y5^b+Ti8~DK1u|5UPQYNK_E(qm3Rrk)${}#CcvhW>@h5)A_*~Xz)3|4f! z)oWdmUE$ih!_nj->dE^DK4oq8aKr<5g5_dD>DO)&=j3sBj_c;(7iKv;o`Yx4^56)B z@LWfEDwDkpbfH!rE$BCA=EBLE!1SzRZoR2+H}C@rCW76izF2FBg=T^m^|sem?s4c; zB`X19*@hYiSejl&w}BpKYvx$~gH@zFRmFm$LBjwF(SL<!2!oXHf0(sabC+JH z$X4?PpPF5fCOfcLdvR3U;TLb>h}{D`2UB2^ps0FEPFfpOC+M8iHR*-Tfnd5)f$qo3 zfql08sQ>1T?b1Hf1j^P&E7$Sq)>-0Gv5UzHa)x0@w5)9Lu_)6!ZlB=9sst3RA&TTl=>B z6q%FlPf}G9;u7jn3hpupVaT(@Bb-D%?L&csShb%1!#VpUBHa^H^^{XK{wsxgw%&Bn zh?=8aU+v0-cLUai3fA46{ODtaJ8?4CD!2U7UIqWaZbq(tp$vGm} zWG(9TNPB-K2#1noP5&nTCIC&mQ}CUVRM{t4#kz@Gwk?O>kSu_UH2TMHddsaUyj}f% z+w6 zbC=3k!90!wNl;?Ph?+hn)?||7HT2Y%Fa-|Rp)KD(kSTa5&aU^gM=H3Ap`#O;_XOj+ zpJ0bhc6Myq&N*N%wz2zFg5%7gr0U3{Vl%HSouk0f*oLheXtL#^KMhOM@!0Rx*y5Ax z)W`9<{cIOyxc-O!nOq!paP5eSiRgt>Mg+){uXAja0xK@JgiK0s-p(seegD3my~|tX z@e6dJ$63@QKmp$0utt!~SgFzhmv#h$!#)s!j`*Ito`2k8JG~aQKB2HK^iKsyDRmx!nbyYucx^yV`F_S{B^dLtS-1=1pMC}cpUwGfpz;~l$k$y z*J!u3e9L#?!1bc+Z>i`*|0;g0!zP2ioQZz2oBv{KT|xECmV`&jyQxzPiONEYd0Nt( zi{^dPWLw;=882}U8=UcL>QPQxwS|wR1OrAqV#}RgUfV#BN^o`VY-nWH} zCl;mXyzd=*#nR*|>Un!6P3wm~tH^#o)ONDYkpUpT%|7^PdN5ksy|?X)_{zq|ie^;P zDc~;CM9o1S`$MtJzflHf^nTFaGMp(%YeN^@z1nbzlSgJpFPJ0I+QHZZtK((|XJRF4o;^BCa3x;(wThsZg>* zIVv^7)dzW)fAK>Qc_bexT_JU5K7!T?RvxM(zoukfzMIdx(i6F0DI9z_i_x8Jvm$=X zJ4m7SCc2Q&!-TDrBAo}7tL`!EPmmxu=O3vg@h++Ovc_^Y zpw7Qf(rh3vIJC!6-&B(;82jo%;*k%!sUN@TGGJZZ5-mcbS(OP$R8>?pD&{GG$0ULq z*!LSruiW*24DS5^hGsBafU4{nIep5*8~B)l1lBjl?s{?e`i42Lx_s&1<;yv02boDV zsj`m`Z!G_=IAn@1soVBrPX@&sZx2gMb!x45i%y`t8b9E8X?>P7Pf)w~No|k#`UEfs z0ElpTBse!EZ7}dRnFmOboqz)77?3aFjN$(d?2#BQ+S{5OPYspx;CO!T+xc4-1TELU zKeHtD{bex~2%U9AN6w30IPo^Yea_y>7ol4rb*s%(#Oo5)-u`YmQo+Ft{tEcPWd$)a zCp#Y`kmSykC_~!tZ7r6}n$+6gXr-PP=Eq3$oXiF!0FS2l)zE{aQz9kaKlIsMNU17W zTs9Lqi}l1cNKdqy1(n|wsr1`pK& z+juU!uH5Oou?h;9tHLy6klML;r@0iatFII|i?*6JqqLnbhFw7Ib1mgmslW5f26g1k ze)$=V1R;ITF6vtp#(wY=Jy*ADxSH%Uz+*2PLd*fY;JETNjJ`Ctda8{EJOcO{#~1hp zAYue1)K3s$7VAt}4rcK!OyhtVfHj$htj&&i5YXgoFW97GUzRYz8KTd^v?!3STu+FY z`m?OeE#xrvE#wN$Rf+j>OKDhZ2 zFYQlR%N=-5nH1am>C=_IOv{Y``&oG7VzY?_dP*7~G+I+plTey+CQkvoYjK2Na6rv` z8z4lTu+BKbycUw&Rb#%fSrBel}M5=@Zu<( z%xp~Q`&UU_)3zS_foJ7$w(N1%Rr82 z5s!mo8zGWA2Y8q@m?HouwQ`urTXW0RmXFI@9!Z}$ZYk=aSO?Xr;PRM%I)DWakljHU z@=cgH`1?7UoDTddeefOSiK1N8eMj>1Ush6KeP7fIQS(~W)EjUwULv)?3kY|(<0PCn{faobLG6x-^h*t2#SMryfeJw}9I^Aa zRb7e@$=LTm=jH}AU=zb9R0BwC8`%jsVK#8W3wvR%w#5?!DAJ)uw8{m539%?8=vMuT z|6B8AvXp^_la4^dUEoXAC{@0(v1tBwWxI9$?*U8-&mIt}27_eM&i~&m0FSh)qo{}L zLX*yvbb0N=2DrNUpW&YJi16Tpc-E1#VEv0kpnY{sWHVr@$~`%cx|+$6jQATu1Z@%9 z(VY#EJ3W?6`JkMo@Y7g98rU%P!1y?auhF6r0LFNI=og+x zIzJLR=QmiVT>BLLX0jWc#0#T%q^Wf%-=wPxOC|TTKdChqE(k$@e*FF`H+y+r`E`?W zP2sx|JrAz%z4&&;pPtbDm*`JXw08koDwNew?sE=Wq=9pOTT0}DE=Gj@W~hJ0b*<|c3j`h*8-ErTj&Wv@Guv(F22Cj_x*73- z+#efw`rV20p7_!zm(xI=Uw5(e(|NWX`WCuJSr0DUt%kY0wIOzAq&rY7zfE+^sPgR| z9PaZaI|5Guoa{#)43q{c^(B8{syY9dX@_V+9v2^Hs9j~=;=5?x!?MGo2e*`%O~@I% zsiF;=I`u{OplQaf`cxxUq$^dQ}bWfcZETDldOH zTJzM61q)X;PaJb*71~EK8+)}~rK2Tbsu#(AGTKm5>*l8UC}Wd3NPSrqclv{+bij?j zmT^~?oy=cbF$rXR)68&87B``Os+g(%;HIIoV zM5y-ky126z~J&y>*>W~`k6YU@9o5Pjq*CSRODnS$b7nF_r1(VtcL&< zrTTW5q)@_p?k#BiKI;BR=7;>~0ASdjyj+NkGO06{j|3Ye+Py_@l#|}bexYwmk=rQM za}I~L>)d1nYQo%Br#HDfws0~NKw5#90-;o}6p;fU|MC&l`p+bGt)ra3KxbIK2viF> zLz6=EkXLrS5&a@hoG-QUIO><)`(duxg~15S3397Ho7_L1zIL}Yb0ndidAU9}cJbH< zO6L?noji)3k%co9d4+@>qwSYh>;*0+9Pb6qc5jUbtN57St`N#)x61s{4c7124P_4? zo;Fa=kVA2|TK_8l(K0u==mrmy!FKif(}yD;>EJ(x^Db!L4&~}Nl)z;ock?FkE)))@ zXy!oiI=jU)=aUIeqc4oYH!(EHPG^m(%nwBT0sQ?v@cEgGP)ikXN%9y49^;lAu*5=hAjb_J z9=i4dIm~OrsbI)Q6zAhRLfY0s-GPCAfkLj?BImiUxHAtXZ#q>W^^$h3pZ<=sD+zBF zN;Tfy8C;D*Pxl5|!^fOgqL>}!r=xALUy%FzVq+6`4&|==>;Bv`KNZLCX6AkUCSM40 z>rXRVjnUPAcUs;)Muh&IarjuHfEq}VKM^kPwidtkiPY*(*qm=SG>P@kTjx-!O-Ycs zF}UPE>SnXBDQ!!31O3Zxd@CC%=gTW^oH>rvoku=OzcWX?nBY#a&nY?`u04GY5+z8U zmD5H==rwJcE|70TtXS-!frKngBG>Z5IFkGnoNoj>;PJ9gY?f2V$1n#9rj9q5)F97N5K9L`u341W3#1y*k_WVlW5G1{MBop7 zu3P|pU42(JNZinRS2kensSvBLikhSdt7m_epcO%K=+n9Ir_E2aFrYnSv%!M568-f0 z3m9}U_Ub`xl>_aMgGAFc0Eu`xLCrYAbMs1)jH`QM&IUu>5-3@ z_c)V>0Ky1tyH4nCp+C#iK2rXN6XH1IN5zdgyRJC$HFg<+Mv@j#Z9w)`U;*WpqY?{T z3Xq6UzF@wOBn9mK$#pQUgi*9dfzp?4ewfSwE~N>QO*hfPjvx;zs&fk!rIy_vFd*%m z6s;BEy3mrQYclj8luJ-%>86x9^hFc+5rrh!qx@= zhhmEWtXvI(5-Eb!0FPZ-KYe?RKQ{M06O9>Xe2SCBpfL44jT1+h!5*NeDf;$m3s?C` zBPyW@AZqqw;gyG(4S)8|c;_JQL`oa;cxCG8_d~^zP4E4HQCFou0UynDAOa4AJ&tA`0la~Ys zdkd{+G64yfpDF@vaj%EKy9)Mb?rG$I4af_Eo2d@MLHE#-H#;aI3muGmw{qcrk5e5v zmnxLntB5tA8lI0MivpC)4V4O@KzY6|3UIUkEQ?{?hSW6Z_~rb7W1Z2RgLB%WE`@3X z75-{HTSp#BXWs|_G(}ah-r}EBZjUqwe*VG=Y|S)i-hiEF9T+UZ3i=PAw+(6*$WBl44&*`{Enp}zA%rjoxIJ*+!L=!JJ|q5_ zZ2uddsy23ZTOgNnL|6h_MP-L>e8>W*cdNlRT<<}%dhc{88_C7=!Ow=o}REcU| z|ExJe0+eq!?s{I?1Ij`WqV|b_UV&a|8YUqtuUXg8f?ys5zGy-nYh-M4U+FClNYA2F zyVk4CBQ5O#WjOZTO%Q;auOBiO%uqc(NR|Q%Zg4JZgNkX>)T6;KuGHdN;WQneQ(PF+ zX_L(SMWLEGz`^WDvEZ`4$5BrscQ0wrMD2P7aly2omJDpxxw?EmRo{WW;%M5R7sPgG z)Q|K@n)@Q!KPwXIVm@%Zr9Gty9ciyIy~V)yL`71{1fXc;hu9Qs&5m5s4PEPsLOAfL zCsZ}FOz;2GGrn)U5M(lQ;b8x?kEXZ7S5cN=2sdp9bW|F`^_P8dx6Jp#yzmxYs@7=; zDkzGT20J1N*NJ!k%ZlJcGi?YppT|rdJc@58aru26eojC1pT6fwQHrWp8#xFj;2`Cr z0JuM}_pC%%qf_*xo`WkSUBuu2el9oR(qOP!Z39};TTZ#hC^>unDd4o09*t;8RT;z- zbJTO1?>aojSmde~3v|jQD53>mL&vj?lhrNK_U>%;mjw2db2Jw$1L_!>dnBR@Z=IEx zxG-SfE(RE*@keX-+$koZYOt;eURyP(7uF7A(OmIeJVX8Y&zP_M&eQL*9^hW!Rxr2o z5EMy(=1y84ALSi0K*;LFyV?>stjO{+;I`7pbLBt~)CJL{DGuknU{-u=y(2Q7>k|^{ zbfk>nuC5;^j7+H9$A|a*kqF2fhx?^Z-}RkITLo8`&5`>Qq7Fg*`o7-~+wjj+YI@+Y z;Kp>tH8mB`DC6Bji8GfCX`m8q7|_@bw~+RB%w4j;b6fS{`zJPQhEPr1O(>m1jxT|f zWT<*>dy0a%_~*_7iE04DLkh!`WY1?6`I~O@oRWb9&Xg%ItB7?SeW;`Te(Wzx9V@kB z4(g7@^PY+1Zx{?zhn@ftlnIU~_TYHu!~nO|XMU3?ym$o5hIFk=?HP{UNf`aa*FYX! zb{V*^R(an?A6}pu{7B9C89zMtp|0z9@B4}@wMEIMK!q*F{k5{)E82&Rs z#3iW9vGDqc`yW+tc)_{9b}@O-2Hduoo`S~pba+X zNvwFqZ**PBlBKF^>drs;xAI+xZorRASEKef(Xeks?rpSdSPswd7Gvq0x|{RdhQXu0X0q>m`#JAu*9)D09+!b?(4b`1 z$+-Em)sc@XAv<|W64qu|3kusH!dD( zb$tI+=veDB!*A~Log2pQ8)4c|ifR7P|Ct4-)UuDw;$_yVqW;&!sHZR?EAKpweX+qh zdFYXUaLj)Z;73=nR=4PU0;&xF+;G+V%r^$n;Za3!eyO}>{)u>x9p$&H01>CJs-azg+#aQCCNlGi% zhHTKGNt3`US(eS`9&2~`Qb>N4`H_@@>2zf%CGRJ+G2;0j0+mENC=D2DLRmZmHlkIm z|DYKY0~-gaTkdY-l~yr|WO(A_J8_jNAu&{#%xqW2ge}d(E4WvId0{%`K;BD{jY<&p z)iR)^gojX@9oA!OR{R*iHf9!n{YK_)X5km2FJOB18LZ@NC>5Pr{MCqWC2^LOA>%t5 z`CU=lpjH0pH<#DY;%&HI;!Lc)I z0g2l6V0e$RCUPFaEDicS1$B$JPzwtv+yC{-CXd&sb2tL>-*+DGM!eEuoZGirh&HB) zlhY!CJNs+Zpa$%TaF_!msKRO1|4TZ*Bn^y4uRV+m-nmX*0i7?g&i06I>kz$VH*ilW zEN)&F!wd!HjX*U*C)g67{iW695l(0ol+ zq8DHpFhkJO8N4h#<1SM+9J_48Y`3mfcLG!sh!HTvtf+ZYr?U%v;dguJF!WZ2S`=R| zUCpAqjgloeGzm3_j1wY)-F)rnEK!MW(i01k7QK8MCF{B=Z!XF!f|iB5HDC^xywWBs z=7h=rK%@Ycb#?U621pQP70be3kI)5ACbn^bnG}`!W*1|sUA^}#TndQK)-x;ICM%I1 zD9}xrk}w_5)cle~{sN9al6Rv5G_y_lh=V|ESrZ6?G-?6*%BAk*7@7`*+7VuS4Y>2S zJ2u6NU-|l|s(uNp0}9pEMpd`?pVVHWJJ1~Hv4e0^ZLb&S5(Q{nnM3$L2rwtbHjf86 z?+uB?clv@@-sa&T&@?|1gLDDCvL^{-22}nHLg8jI0KD4Dx(|n&>Kkjv!HBfM8_Rl%yqwd?2`@fENFlQAPtvlM*rAfnPnXCAqAM2t(g!^5CNF7 z+M(3IRmU|KFm$5QuAQFv2+ZLB&p$nR@817udq?swTkRuI;gkpm)u9V7SAa6$cu)lh zM-2H0pl^dSmpvSS-{o<%0EPs=+bDM@1)94$xQQ&$>}sWW)(*XN|7xY3o_ZIK0`=x) z_T^@9COt@?kv~j^cus!tU#4p|wpW1XDR_v2-P#aABLr(-ujM6;!*t&E))W>3{TaA& zk6`C_@*Uvt(uiK5G=v7&qWH&Myu#`*$4YpTd>X0s5dqiiTF;K>*5R-7>wqFgQS)|a z!HFK%L4i4kqnuu6`dmDU!6Yu@nffvY*nFL@325g{e+S{eW9<*W`%?TKV8}r+d(;uo z!6jfs8=206>6A~QbUBU6e0f4Ukli4q$=e7_N*iCW^#>NOkvXB>GsO8aF|N|^>i}cx z@99`}JzMM9^x6yrI;^p2#xz%EdJTp_U;^`ODwUsgJp7^FJrbx=nG~;n(JYrs&a^)b zSI^44yJhcoKydCAPm#2>=0nF3wR#P9*gL3952N2H@?JZ#3>yK>Si!#VLMGw6>1V?jp_ zT3EyDQik_*pH{~+sg+Q-#GQTHpvZdqHH6)s3dHY1<^SXtm6piriXPY`7%rn;-O!7x zIL-@#`-y!=-kOE$>E<}DTyv3%%h)UPCL#>KANr|N8K&b;IBUa?i_b&qW+>oEt999E z3HaIo9DA^MhbgLloNF5@d-D;}pgTLipZps{M|swXaNI3x&>yxjz|%}mF zAVj{s*%3wt0~VcuOXv)?`NjWT%+I$&?X;)|)~a5+uB&fMcYg%v$MJ$0LJX#LGV_U$?d>v_Sb{O=mFzE zy*fC-G!8n^_05*hs1X}hHHdLP*6F(5SV<-)e6MSLmBt0S_hadxq3PRvfy@G=3Csx* zPG2zO;Rw1VSt-jUX%)v4Mm{+ncgy7hjjUE7)5Uy^dW~P5KBl3w%H&7j1(yjRPbgWT zq5uqm+QFCd8mgmsIncN0p?vX*H?&XSi2zp+E@QiBnp-W-V6