From e026a27b9380467eed8ae2d2ecc082394f77cc2d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 12 Dec 2022 12:11:33 +0100 Subject: [PATCH 01/47] hiero: fixing thumbnail if multillayer exr --- openpype/hosts/hiero/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/plugins/publish/extract_thumbnail.py b/openpype/hosts/hiero/plugins/publish/extract_thumbnail.py index e64aa89b26..5ca79dc1dc 100644 --- a/openpype/hosts/hiero/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/hiero/plugins/publish/extract_thumbnail.py @@ -41,7 +41,7 @@ class ExtractThumnail(publish.Extractor): track_item_name, thumb_frame, ".png") thumb_path = os.path.join(staging_dir, thumb_file) - thumbnail = track_item.thumbnail(thumb_frame).save( + thumbnail = track_item.thumbnail(thumb_frame, "colour").save( thumb_path, format='png' ) From 95b15a9f00bbcd5f7bd0e241e3818ee6b6d64c9d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 22 Dec 2022 11:44:35 +0100 Subject: [PATCH 02/47] global,nuke,maya: on demand placeholder removal preset attribute --- .../maya/api/workfile_template_builder.py | 2 +- .../nuke/api/workfile_template_builder.py | 15 ++++-- .../workfile/workfile_template_builder.py | 49 +++++++++++++++---- .../schema_templated_workfile_build.json | 11 ++++- 4 files changed, 62 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index ef043ed0f4..1d3f1cf568 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -28,7 +28,7 @@ class MayaTemplateBuilder(AbstractTemplateBuilder): Args: path (str): A path to current template (usually given by - get_template_path implementation) + get_template_preset implementation) Returns: bool: Wether the template was succesfully imported or not diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index 7a2e442e32..60bf906fbe 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -40,7 +40,7 @@ class NukeTemplateBuilder(AbstractTemplateBuilder): Args: path (str): A path to current template (usually given by - get_template_path implementation) + get_template_preset implementation) Returns: bool: Wether the template was succesfully imported or not @@ -273,6 +273,15 @@ class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin): placeholder.data["nb_children"] += 1 reset_selection() + + # remove placeholders marked as delete + if ( + placeholder.data.get("delete") + and not placeholder.data.get("keep_placeholder") + ): + self.log.debug("Deleting node: {}".format(placeholder_node.name())) + nuke.delete(placeholder_node) + # go back to root group nuke.root().begin() @@ -454,12 +463,12 @@ class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin): ) for node in placeholder_node.dependent(): for idx in range(node.inputs()): - if node.input(idx) == placeholder_node: + if node.input(idx) == placeholder_node and output_node: node.setInput(idx, output_node) for node in placeholder_node.dependencies(): for idx in range(placeholder_node.inputs()): - if placeholder_node.input(idx) == node: + if placeholder_node.input(idx) == node and input_node: input_node.setInput(0, node) def _create_sib_copies(self, placeholder): diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 582657c735..f6a4ab51cb 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -401,7 +401,12 @@ class AbstractTemplateBuilder(object): key=lambda i: i.order )) - def build_template(self, template_path=None, level_limit=None): + def build_template( + self, + template_path=None, + level_limit=None, + keep_placeholders=None + ): """Main callback for building workfile from template path. Todo: @@ -410,16 +415,22 @@ class AbstractTemplateBuilder(object): Args: template_path (str): Path to a template file with placeholders. - Template from settings 'get_template_path' used when not + Template from settings 'get_template_preset' used when not passed. level_limit (int): Limit of populate loops. Related to 'populate_scene_placeholders' method. """ + template_preset = self.get_template_preset() if template_path is None: - template_path = self.get_template_path() + template_path = template_preset["path"] + + if keep_placeholders is None: + keep_placeholders = template_preset["placeholder_keep"] + self.import_template(template_path) - self.populate_scene_placeholders(level_limit) + self.populate_scene_placeholders( + level_limit, keep_placeholders) def rebuild_template(self): """Go through existing placeholders in scene and update them. @@ -489,7 +500,9 @@ class AbstractTemplateBuilder(object): plugin = plugins_by_identifier[identifier] plugin.prepare_placeholders(placeholders) - def populate_scene_placeholders(self, level_limit=None): + def populate_scene_placeholders( + self, level_limit=None, keep_placeholders=None + ): """Find placeholders in scene using plugins and process them. This should happen after 'import_template'. @@ -541,6 +554,11 @@ class AbstractTemplateBuilder(object): " is already in progress." )) continue + + # add flag for keeping placeholders in scene + # after they are processed + placeholder.data["keep_placeholder"] = keep_placeholders + filtered_placeholders.append(placeholder) self._prepare_placeholders(filtered_placeholders) @@ -599,8 +617,8 @@ class AbstractTemplateBuilder(object): ["profiles"] ) - def get_template_path(self): - """Unified way how template path is received usign settings. + def get_template_preset(self): + """Unified way how template preset is received usign settings. Method is dependent on '_get_build_profiles' which should return filter profiles to resolve path to a template. Default implementation looks @@ -637,6 +655,13 @@ class AbstractTemplateBuilder(object): ).format(task_name, task_type, host_name)) path = profile["path"] + + # switch to remove placeholders after they are used + placeholder_keep = profile.get("placeholder_keep") + # backward compatibility, since default is True + if placeholder_keep is not False: + placeholder_keep = True + if not path: raise TemplateLoadFailed(( "Template path is not set.\n" @@ -657,7 +682,10 @@ class AbstractTemplateBuilder(object): if path and os.path.exists(path): self.log.info("Found template at: '{}'".format(path)) - return path + return { + "path": path, + "placeholder_keep": placeholder_keep + } solved_path = None while True: @@ -683,7 +711,10 @@ class AbstractTemplateBuilder(object): self.log.info("Found template at: '{}'".format(solved_path)) - return solved_path + return { + "path": solved_path, + "placeholder_keep": placeholder_keep + } @six.add_metaclass(ABCMeta) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json index 99a29beb27..1826734291 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json @@ -25,8 +25,15 @@ { "key": "path", "label": "Path to template", - "type": "text", - "object_type": "text" + "type": "path", + "multiplatform": false, + "multipath": false + }, + { + "key": "placeholder_keep", + "label": "Keep placeholders", + "type": "boolean", + "default": true } ] } From 011cd8f2e4e9536ce59194668d91b92712484ea1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 22 Dec 2022 11:45:38 +0100 Subject: [PATCH 03/47] nuke: remove update template menu item --- openpype/hosts/nuke/api/pipeline.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index fb707ca44c..918598c04f 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -217,10 +217,6 @@ def _install_menu(): "Build Workfile from template", lambda: build_workfile_template() ) - menu_template.addCommand( - "Update Workfile", - lambda: update_workfile_template() - ) menu_template.addSeparator() menu_template.addCommand( "Create Place Holder", From 90303d4137d3dfca49035ac1c878dbb830f42b42 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 22 Dec 2022 11:48:46 +0100 Subject: [PATCH 04/47] global: updating docstrings --- openpype/pipeline/workfile/workfile_template_builder.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index f6a4ab51cb..2850175bc9 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -419,6 +419,9 @@ class AbstractTemplateBuilder(object): passed. level_limit (int): Limit of populate loops. Related to 'populate_scene_placeholders' method. + keep_placeholders (bool): Add flag to placeholder data for + hosts to decide if they want to remove + placeholder after it is used. """ template_preset = self.get_template_preset() @@ -518,6 +521,9 @@ class AbstractTemplateBuilder(object): Args: level_limit (int): Level of loops that can happen. Default is 1000. + keep_placeholders (bool): Add flag to placeholder data for + hosts to decide if they want to remove + placeholder after it is used. """ if not self.placeholder_plugins: From 212b372c03dad76e7aa1a7b83148ea78dda5611e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 22 Dec 2022 11:49:18 +0100 Subject: [PATCH 05/47] nuke: make `get_group_io_nodes` soft fail --- openpype/hosts/nuke/api/lib.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index a066bbcdcf..2fdf446357 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2865,10 +2865,11 @@ def get_group_io_nodes(nodes): break if input_node is None: - raise ValueError("No Input found") + log.warning("No Input found") if output_node is None: - raise ValueError("No Output found") + log.warning("No Output found") + return input_node, output_node From f5cb893dc1b28b62b8132796bf5037d1236d6341 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Dec 2022 12:16:54 +0100 Subject: [PATCH 06/47] global: creator plugin abstraction for workfile builder template --- openpype/hosts/nuke/api/pipeline.py | 4 +- .../workfile/workfile_template_builder.py | 191 +++++++++++++++++- 2 files changed, 192 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 918598c04f..bdf12b7dc4 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -35,6 +35,7 @@ from .lib import ( ) from .workfile_template_builder import ( NukePlaceholderLoadPlugin, + NukePlaceholderCreatePlugin, build_workfile_template, update_workfile_template, create_placeholder, @@ -139,7 +140,8 @@ def _show_workfiles(): def get_workfile_build_placeholder_plugins(): return [ - NukePlaceholderLoadPlugin + NukePlaceholderLoadPlugin, + NukePlaceholderCreatePlugin ] diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 2850175bc9..d85d6b50dd 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -42,7 +42,10 @@ from openpype.pipeline.load import ( get_contexts_for_repre_docs, load_with_repre_context, ) -from openpype.pipeline.create import get_legacy_creator_by_name +from openpype.pipeline.create import ( + get_legacy_creator_by_name, + discover_legacy_creator_plugins +) class TemplateNotFound(Exception): @@ -235,7 +238,7 @@ class AbstractTemplateBuilder(object): def get_creators_by_name(self): if self._creators_by_name is None: - self._creators_by_name = get_legacy_creator_by_name() + self._creators_by_name = discover_legacy_creator_plugins() return self._creators_by_name def get_shared_data(self, key): @@ -1463,6 +1466,165 @@ class PlaceholderLoadMixin(object): pass +class PlaceholderCreateMixin(object): + """Mixin prepared for creating placeholder plugins. + + Implementation prepares options for placeholders with + 'get_create_plugin_options'. + + For placeholder population is implemented 'populate_create_placeholder'. + + PlaceholderItem can have implemented methods: + - 'create_failed' - called when creating of an instance failed + - 'create_succeed' - called when creating of an instance succeeded + """ + + def get_create_plugin_options(self, options=None): + """Unified attribute definitions for create placeholder. + + Common function for placeholder plugins used for creating of + publishable instances. Use it with 'get_placeholder_options'. + + Args: + plugin (PlaceholderPlugin): Plugin used for creating of + publish instances. + options (Dict[str, Any]): Already available options which are used + as defaults for attributes. + + Returns: + List[AbtractAttrDef]: Attribute definitions common for create + plugins. + """ + + creators_by_name = self.builder.get_creators_by_name() + creator_items = [ + (creator_name, creator.label or creator_name) + for creator_name, creator in creators_by_name.items() + ] + + creator_items = list(sorted(creator_items, key=lambda i: i[1])) + options = options or {} + return [ + attribute_definitions.UISeparatorDef(), + attribute_definitions.UILabelDef("Main attributes"), + attribute_definitions.UISeparatorDef(), + + attribute_definitions.EnumDef( + "creator", + label="Creator", + default=options.get("creator"), + items=creator_items, + tooltip=( + "Creator" + "\nDefines what OpenPype creator will be used to" + " create publishable instance." + "\nUseable creator depends on current host's creator list." + "\nField is case sensitive." + ) + ), + attribute_definitions.TextDef( + "create_variant", + label="Variant", + default=options.get("create_variant"), + placeholder='Main', + tooltip=( + "Creator" + "\nDefines variant name which will be use for " + "\ncompiling of subset name." + ) + ), + attribute_definitions.UISeparatorDef(), + attribute_definitions.NumberDef( + "order", + label="Order", + default=options.get("order") or 0, + decimals=0, + minimum=0, + maximum=999, + tooltip=( + "Order" + "\nOrder defines creating instance priority (0 to 999)" + "\nPriority rule is : \"lowest is first to load\"." + ) + ) + ] + + def populate_create_placeholder(self, placeholder): + """Create placeholder is going to create matching publishabe instance. + + Args: + placeholder (PlaceholderItem): Placeholder item with information + about requested publishable instance. + """ + creator_name = placeholder.data["creator"] + create_variant = placeholder.data["create_variant"] + + creator_plugin = get_legacy_creator_by_name(creator_name) + + # create subset name + project_name = legacy_io.Session["AVALON_PROJECT"] + task_name = legacy_io.Session["AVALON_TASK"] + asset_name = legacy_io.Session["AVALON_ASSET"] + + # get asset id + asset_doc = get_asset_by_name(project_name, asset_name, fields=["_id"]) + assert asset_doc, "No current asset found in Session" + asset_id = asset_doc['_id'] + + subset_name = creator_plugin.get_subset_name( + create_variant, + task_name, + asset_id, + project_name + ) + + creator_data = { + "creator_name": creator_name, + "create_variant": create_variant, + "subset_name": subset_name, + "creator_plugin": creator_plugin + } + + # compile subset name from variant + try: + creator_instance = creator_plugin( + subset_name, + asset_name + ).process() + + except Exception: + failed = True + self.create_failed(placeholder, creator_data) + + else: + failed = False + self.create_succeed(placeholder, creator_instance) + + self.cleanup_placeholder(placeholder, failed) + + def create_failed(self, placeholder, creator_data): + if hasattr(placeholder, "create_failed"): + placeholder.create_failed(creator_data) + + def create_succeed(self, placeholder, creator_instance): + if hasattr(placeholder, "create_succeed"): + placeholder.create_succeed(creator_instance) + + def cleanup_placeholder(self, placeholder, failed): + """Cleanup placeholder after load of single representation. + + Can be called multiple times during placeholder item populating and is + called even if loading failed. + + Args: + placeholder (PlaceholderItem): Item which was just used to load + representation. + failed (bool): Loading of representation failed. + """ + + pass + + class LoadPlaceholderItem(PlaceholderItem): """PlaceholderItem for plugin which is loading representations. @@ -1486,3 +1648,28 @@ class LoadPlaceholderItem(PlaceholderItem): def load_failed(self, representation): self._failed_representations.append(representation) + + +class CreatePlaceholderItem(PlaceholderItem): + """PlaceholderItem for plugin which is creating publish instance. + + Connected to 'PlaceholderCreateMixin'. + """ + + def __init__(self, *args, **kwargs): + super(CreatePlaceholderItem, self).__init__(*args, **kwargs) + self._failed_created_publish_instances = [] + + def get_errors(self): + if not self._failed_representations: + return [] + message = ( + "Failed to create {} instance using Creator {}" + ).format( + len(self._failed_created_publish_instances), + self.data["creator"] + ) + return [message] + + def create_failed(self, creator_data): + self._failed_created_publish_instances.append(creator_data) From dfb3d142aa765e27a4fc2eb3c861e70ad57fb464 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Dec 2022 12:17:27 +0100 Subject: [PATCH 07/47] nuke: workfile builder template creator plugin implementation --- .../nuke/api/workfile_template_builder.py | 410 +++++++++++++++++- 1 file changed, 409 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index 60bf906fbe..5e9e5fcdce 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -7,7 +7,9 @@ from openpype.pipeline.workfile.workfile_template_builder import ( AbstractTemplateBuilder, PlaceholderPlugin, LoadPlaceholderItem, + CreatePlaceholderItem, PlaceholderLoadMixin, + PlaceholderCreateMixin ) from openpype.tools.workfile_template_build import ( WorkfileBuildPlaceholderDialog, @@ -32,7 +34,7 @@ PLACEHOLDER_SET = "PLACEHOLDERS_SET" class NukeTemplateBuilder(AbstractTemplateBuilder): - """Concrete implementation of AbstractTemplateBuilder for maya""" + """Concrete implementation of AbstractTemplateBuilder for nuke""" def import_template(self, path): """Import template into current scene. @@ -544,6 +546,412 @@ class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin): siblings_input.setInput(0, copy_output) +class NukePlaceholderCreatePlugin( + NukePlaceholderPlugin, PlaceholderCreateMixin +): + identifier = "nuke.create" + label = "Nuke create" + + def _parse_placeholder_node_data(self, node): + placeholder_data = super( + NukePlaceholderCreatePlugin, self + )._parse_placeholder_node_data(node) + + node_knobs = node.knobs() + nb_children = 0 + if "nb_children" in node_knobs: + nb_children = int(node_knobs["nb_children"].getValue()) + placeholder_data["nb_children"] = nb_children + + siblings = [] + if "siblings" in node_knobs: + siblings = node_knobs["siblings"].values() + placeholder_data["siblings"] = siblings + + node_full_name = node.fullName() + placeholder_data["group_name"] = node_full_name.rpartition(".")[0] + placeholder_data["last_loaded"] = [] + placeholder_data["delete"] = False + return placeholder_data + + def collect_placeholders(self): + output = [] + scene_placeholders = self._collect_scene_placeholders() + for node_name, node in scene_placeholders.items(): + plugin_identifier_knob = node.knob("plugin_identifier") + if ( + plugin_identifier_knob is None + or plugin_identifier_knob.getValue() != self.identifier + ): + continue + + placeholder_data = self._parse_placeholder_node_data(node) + # TODO do data validations and maybe updgrades if are invalid + output.append( + CreatePlaceholderItem(node_name, placeholder_data, self) + ) + + return output + + def populate_placeholder(self, placeholder): + self.populate_create_placeholder(placeholder) + + def repopulate_placeholder(self, placeholder): + self.populate_create_placeholder(placeholder) + + def get_placeholder_options(self, options=None): + return self.get_create_plugin_options(options) + + def cleanup_placeholder(self, placeholder, failed): + # deselect all selected nodes + placeholder_node = nuke.toNode(placeholder.scene_identifier) + + # getting the latest nodes added + nodes_init = placeholder.data["nodes_init"] + nodes_created = list(set(nuke.allNodes()) - set(nodes_init)) + self.log.debug("Created nodes: {}".format(nodes_created)) + if not nodes_created: + return + + placeholder.data["delete"] = True + + nodes_created = self._move_to_placeholder_group( + placeholder, nodes_created + ) + placeholder.data["last_created"] = nodes_created + refresh_nodes(nodes_created) + + # positioning of the created nodes + min_x, min_y, _, _ = get_extreme_positions(nodes_created) + for node in nodes_created: + xpos = (node.xpos() - min_x) + placeholder_node.xpos() + ypos = (node.ypos() - min_y) + placeholder_node.ypos() + node.setXYpos(xpos, ypos) + refresh_nodes(nodes_created) + + # fix the problem of z_order for backdrops + self._fix_z_order(placeholder) + self._imprint_siblings(placeholder) + + if placeholder.data["nb_children"] == 0: + # save initial nodes postions and dimensions, update them + # and set inputs and outputs of created nodes + + self._imprint_inits() + self._update_nodes(placeholder, nuke.allNodes(), nodes_created) + self._set_created_connections(placeholder) + + elif placeholder.data["siblings"]: + # create copies of placeholder siblings for the new created nodes, + # set their inputs and outpus and update all nodes positions and + # dimensions and siblings names + + siblings = get_nodes_by_names(placeholder.data["siblings"]) + refresh_nodes(siblings) + copies = self._create_sib_copies(placeholder) + new_nodes = list(copies.values()) # copies nodes + self._update_nodes(new_nodes, nodes_created) + placeholder_node.removeKnob(placeholder_node.knob("siblings")) + new_nodes_name = get_names_from_nodes(new_nodes) + imprint(placeholder_node, {"siblings": new_nodes_name}) + self._set_copies_connections(placeholder, copies) + + self._update_nodes( + nuke.allNodes(), + new_nodes + nodes_created, + 20 + ) + + new_siblings = get_names_from_nodes(new_nodes) + placeholder.data["siblings"] = new_siblings + + else: + # if the placeholder doesn't have siblings, the created + # nodes will be placed in a free space + + xpointer, ypointer = find_free_space_to_paste_nodes( + nodes_created, direction="bottom", offset=200 + ) + node = nuke.createNode("NoOp") + reset_selection() + nuke.delete(node) + for node in nodes_created: + xpos = (node.xpos() - min_x) + xpointer + ypos = (node.ypos() - min_y) + ypointer + node.setXYpos(xpos, ypos) + + placeholder.data["nb_children"] += 1 + reset_selection() + + # remove placeholders marked as delete + if ( + placeholder.data.get("delete") + and not placeholder.data.get("keep_placeholder") + ): + self.log.debug("Deleting node: {}".format(placeholder_node.name())) + nuke.delete(placeholder_node) + + # go back to root group + nuke.root().begin() + + def _move_to_placeholder_group(self, placeholder, nodes_created): + """ + opening the placeholder's group and copying created nodes in it. + + Returns : + nodes_created (list): the new list of pasted nodes + """ + groups_name = placeholder.data["group_name"] + reset_selection() + select_nodes(nodes_created) + if groups_name: + with node_tempfile() as filepath: + nuke.nodeCopy(filepath) + for node in nuke.selectedNodes(): + nuke.delete(node) + group = nuke.toNode(groups_name) + group.begin() + nuke.nodePaste(filepath) + nodes_created = nuke.selectedNodes() + return nodes_created + + def _fix_z_order(self, placeholder): + """Fix the problem of z_order when a backdrop is create.""" + + nodes_created = placeholder.data["last_created"] + created_backdrops = [] + bd_orders = set() + for node in nodes_created: + if isinstance(node, nuke.BackdropNode): + created_backdrops.append(node) + bd_orders.add(node.knob("z_order").getValue()) + + if not bd_orders: + return + + sib_orders = set() + for node_name in placeholder.data["siblings"]: + node = nuke.toNode(node_name) + if isinstance(node, nuke.BackdropNode): + sib_orders.add(node.knob("z_order").getValue()) + + if not sib_orders: + return + + min_order = min(bd_orders) + max_order = max(sib_orders) + for backdrop_node in created_backdrops: + z_order = backdrop_node.knob("z_order").getValue() + backdrop_node.knob("z_order").setValue( + z_order + max_order - min_order + 1) + + def _imprint_siblings(self, placeholder): + """ + - add siblings names to placeholder attributes (nodes created with it) + - add Id to the attributes of all the other nodes + """ + + created_nodes = placeholder.data["last_created"] + created_nodes_set = set(created_nodes) + data = {"repre_id": str(placeholder.data["last_repre_id"])} + + for node in created_nodes: + node_knobs = node.knobs() + if "builder_type" not in node_knobs: + # save the id of representation for all imported nodes + imprint(node, data) + node.knob("repre_id").setVisible(False) + refresh_node(node) + continue + + if ( + "is_placeholder" not in node_knobs + or ( + "is_placeholder" in node_knobs + and node.knob("is_placeholder").value() + ) + ): + siblings = list(created_nodes_set - {node}) + siblings_name = get_names_from_nodes(siblings) + siblings = {"siblings": siblings_name} + imprint(node, siblings) + + def _imprint_inits(self): + """Add initial positions and dimensions to the attributes""" + + for node in nuke.allNodes(): + refresh_node(node) + imprint(node, {"x_init": node.xpos(), "y_init": node.ypos()}) + node.knob("x_init").setVisible(False) + node.knob("y_init").setVisible(False) + width = node.screenWidth() + height = node.screenHeight() + if "bdwidth" in node.knobs(): + imprint(node, {"w_init": width, "h_init": height}) + node.knob("w_init").setVisible(False) + node.knob("h_init").setVisible(False) + refresh_node(node) + + def _update_nodes( + self, placeholder, nodes, considered_nodes, offset_y=None + ): + """Adjust backdrop nodes dimensions and positions. + + Considering some nodes sizes. + + Args: + nodes (list): list of nodes to update + considered_nodes (list): list of nodes to consider while updating + positions and dimensions + offset (int): distance between copies + """ + + placeholder_node = nuke.toNode(placeholder.scene_identifier) + + min_x, min_y, max_x, max_y = get_extreme_positions(considered_nodes) + + diff_x = diff_y = 0 + contained_nodes = [] # for backdrops + + if offset_y is None: + width_ph = placeholder_node.screenWidth() + height_ph = placeholder_node.screenHeight() + diff_y = max_y - min_y - height_ph + diff_x = max_x - min_x - width_ph + contained_nodes = [placeholder_node] + min_x = placeholder_node.xpos() + min_y = placeholder_node.ypos() + else: + siblings = get_nodes_by_names(placeholder.data["siblings"]) + minX, _, maxX, _ = get_extreme_positions(siblings) + diff_y = max_y - min_y + 20 + diff_x = abs(max_x - min_x - maxX + minX) + contained_nodes = considered_nodes + + if diff_y <= 0 and diff_x <= 0: + return + + for node in nodes: + refresh_node(node) + + if ( + node == placeholder_node + or node in considered_nodes + ): + continue + + if ( + not isinstance(node, nuke.BackdropNode) + or ( + isinstance(node, nuke.BackdropNode) + and not set(contained_nodes) <= set(node.getNodes()) + ) + ): + if offset_y is None and node.xpos() >= min_x: + node.setXpos(node.xpos() + diff_x) + + if node.ypos() >= min_y: + node.setYpos(node.ypos() + diff_y) + + else: + width = node.screenWidth() + height = node.screenHeight() + node.knob("bdwidth").setValue(width + diff_x) + node.knob("bdheight").setValue(height + diff_y) + + refresh_node(node) + + def _set_created_connections(self, placeholder): + """ + set inputs and outputs of created nodes""" + + placeholder_node = nuke.toNode(placeholder.scene_identifier) + input_node, output_node = get_group_io_nodes( + placeholder.data["last_created"] + ) + for node in placeholder_node.dependent(): + for idx in range(node.inputs()): + if node.input(idx) == placeholder_node and output_node: + node.setInput(idx, output_node) + + for node in placeholder_node.dependencies(): + for idx in range(placeholder_node.inputs()): + if placeholder_node.input(idx) == node and input_node: + input_node.setInput(0, node) + + def _create_sib_copies(self, placeholder): + """ creating copies of the palce_holder siblings (the ones who were + created with it) for the new nodes added + + Returns : + copies (dict) : with copied nodes names and their copies + """ + + copies = {} + siblings = get_nodes_by_names(placeholder.data["siblings"]) + for node in siblings: + new_node = duplicate_node(node) + + x_init = int(new_node.knob("x_init").getValue()) + y_init = int(new_node.knob("y_init").getValue()) + new_node.setXYpos(x_init, y_init) + if isinstance(new_node, nuke.BackdropNode): + w_init = new_node.knob("w_init").getValue() + h_init = new_node.knob("h_init").getValue() + new_node.knob("bdwidth").setValue(w_init) + new_node.knob("bdheight").setValue(h_init) + refresh_node(node) + + if "repre_id" in node.knobs().keys(): + node.removeKnob(node.knob("repre_id")) + copies[node.name()] = new_node + return copies + + def _set_copies_connections(self, placeholder, copies): + """Set inputs and outputs of the copies. + + Args: + copies (dict): Copied nodes by their names. + """ + + last_input, last_output = get_group_io_nodes( + placeholder.data["last_created"] + ) + siblings = get_nodes_by_names(placeholder.data["siblings"]) + siblings_input, siblings_output = get_group_io_nodes(siblings) + copy_input = copies[siblings_input.name()] + copy_output = copies[siblings_output.name()] + + for node_init in siblings: + if node_init == siblings_output: + continue + + node_copy = copies[node_init.name()] + for node in node_init.dependent(): + for idx in range(node.inputs()): + if node.input(idx) != node_init: + continue + + if node in siblings: + copies[node.name()].setInput(idx, node_copy) + else: + last_input.setInput(0, node_copy) + + for node in node_init.dependencies(): + for idx in range(node_init.inputs()): + if node_init.input(idx) != node: + continue + + if node_init == siblings_input: + copy_input.setInput(idx, node) + elif node in siblings: + node_copy.setInput(idx, copies[node.name()]) + else: + node_copy.setInput(idx, last_output) + + siblings_input.setInput(0, copy_output) + + def build_workfile_template(*args): builder = NukeTemplateBuilder(registered_host()) builder.build_template() From 013ee5660af7dcb2b88499edf8d4d6dfcf3a259c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Dec 2022 12:17:39 +0100 Subject: [PATCH 08/47] fix typo --- openpype/hosts/nuke/plugins/load/load_backdrop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/load/load_backdrop.py b/openpype/hosts/nuke/plugins/load/load_backdrop.py index 164ab6f9f4..d1fb763500 100644 --- a/openpype/hosts/nuke/plugins/load/load_backdrop.py +++ b/openpype/hosts/nuke/plugins/load/load_backdrop.py @@ -28,7 +28,7 @@ class LoadBackdropNodes(load.LoaderPlugin): representations = ["nk"] families = ["workfile", "nukenodes"] - label = "Iport Nuke Nodes" + label = "Import Nuke Nodes" order = 0 icon = "eye" color = "white" From 410ed90cb33348ee38525a3045612ef2a19f1970 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Dec 2022 12:17:54 +0100 Subject: [PATCH 09/47] fix typo --- openpype/pipeline/create/creator_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index bb5ce00452..8500dd1e22 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -608,7 +608,7 @@ def discover_legacy_creator_plugins(): plugin.apply_settings(project_settings, system_settings) except Exception: log.warning( - "Failed to apply settings to loader {}".format( + "Failed to apply settings to creator {}".format( plugin.__name__ ), exc_info=True From ae709afaaf85ca6bd1d6d74476ea8c561d550eec Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 23 Dec 2022 12:25:01 +0100 Subject: [PATCH 10/47] Added dynamic message to Slack notification Artist can now add additional message, specific per instance and publish, if they are using Publisher. --- .../plugins/publish/collect_slack_family.py | 23 +++++++++++++++++-- .../plugins/publish/integrate_slack_api.py | 11 +++++---- website/docs/module_slack.md | 6 +++++ 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/collect_slack_family.py b/openpype/modules/slack/plugins/publish/collect_slack_family.py index 27e899d59a..b3e7bbdcec 100644 --- a/openpype/modules/slack/plugins/publish/collect_slack_family.py +++ b/openpype/modules/slack/plugins/publish/collect_slack_family.py @@ -1,10 +1,12 @@ import pyblish.api from openpype.lib.profiles_filtering import filter_profiles -from openpype.pipeline import legacy_io +from openpype.lib import attribute_definitions +from openpype.pipeline import OpenPypePyblishPluginMixin -class CollectSlackFamilies(pyblish.api.InstancePlugin): +class CollectSlackFamilies(pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin): """Collect family for Slack notification Expects configured profile in @@ -17,6 +19,18 @@ class CollectSlackFamilies(pyblish.api.InstancePlugin): profiles = None + @classmethod + def get_attribute_defs(cls): + return [ + attribute_definitions.TextDef( + # Key under which it will be stored + "additional_message", + # Use plugin label as label for attribute + label="Additional Slack message", + placeholder="" + ) + ] + def process(self, instance): task_data = instance.data["anatomyData"].get("task", {}) family = self.main_family_from_instance(instance) @@ -55,6 +69,11 @@ class CollectSlackFamilies(pyblish.api.InstancePlugin): ["token"]) instance.data["slack_token"] = slack_token + attribute_values = self.get_attr_values_from_data(instance.data) + additional_message = attribute_values.get("additional_message") + if additional_message: + instance.data["slack_additional_message"] = additional_message + def main_family_from_instance(self, instance): # TODO yank from integrate """Returns main family of entered instance.""" family = instance.data.get("family") diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 0cd5ec9de8..d94ecb02e4 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -31,11 +31,14 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): review_path = self._get_review_path(instance) publish_files = set() + message = '' + additional_message = instance.data.get("slack_additional_message") + if additional_message: + message = "{} \n".format(additional_message) for message_profile in instance.data["slack_channel_message_profiles"]: - message = self._get_filled_message(message_profile["message"], - instance, - review_path) - self.log.debug("message:: {}".format(message)) + message += self._get_filled_message(message_profile["message"], + instance, + review_path) if not message: return diff --git a/website/docs/module_slack.md b/website/docs/module_slack.md index 3a2842da63..2bfd7cb562 100644 --- a/website/docs/module_slack.md +++ b/website/docs/module_slack.md @@ -94,6 +94,12 @@ Few keys also have Capitalized and UPPERCASE format. Values will be modified acc Here you can find review {review_filepath} ``` +##### Dynamic message for artists +If artists uses host with implemented Publisher (new UI for publishing, implemented in Tray Publisher, Adobe products etc), it is possible for +them to add additional message (notification for specific users for example, artists must provide proper user id with '@'). +Additional message will be sent only if at least one profile, eg. one target channel is configured. +All available template keys (see higher) could be used here as a placeholder too. + #### Message retention Currently no purging of old messages is implemented in Openpype. Admins of Slack should set their own retention of messages and files per channel. (see https://slack.com/help/articles/203457187-Customize-message-and-file-retention-policies) From c6c08fd4ccaf779c2569139ab83f8fb16fe6c785 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Dec 2022 12:46:14 +0100 Subject: [PATCH 11/47] global: fix creator plugin discovery --- .../pipeline/workfile/workfile_template_builder.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index d85d6b50dd..07a1f3ec58 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -238,7 +238,14 @@ class AbstractTemplateBuilder(object): def get_creators_by_name(self): if self._creators_by_name is None: - self._creators_by_name = discover_legacy_creator_plugins() + self._creators_by_name = {} + for creator in discover_legacy_creator_plugins(): + creator_name = creator.__name__ + if creator_name in self._creators_by_name: + raise KeyError( + "Duplicated creator name {} !".format(creator_name) + ) + self._creators_by_name[creator_name] = creator return self._creators_by_name def get_shared_data(self, key): @@ -1497,6 +1504,8 @@ class PlaceholderCreateMixin(object): """ creators_by_name = self.builder.get_creators_by_name() + print(creators_by_name) + creator_items = [ (creator_name, creator.label or creator_name) for creator_name, creator in creators_by_name.items() From 81de2cf0c902671104de22f7768ed37ac8ed5c39 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Dec 2022 14:25:41 +0100 Subject: [PATCH 12/47] global: fix _repr_ pformat printing --- openpype/pipeline/workfile/workfile_template_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 07a1f3ec58..630a11e4b5 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -984,7 +984,7 @@ class PlaceholderItem(object): def __init__(self, scene_identifier, data, plugin): self._log = None - self._scene_identifier = scene_identifier + self.name = scene_identifier self._data = data self._plugin = plugin @@ -1062,7 +1062,7 @@ class PlaceholderItem(object): @property def scene_identifier(self): - return self._scene_identifier + return self.name @property def finished(self): From 4f7e4fcac3ffbddc258247941b063f25746ef174 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Dec 2022 14:26:28 +0100 Subject: [PATCH 13/47] global: add _before_instance_create function for storing created nodes --- openpype/pipeline/workfile/workfile_template_builder.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 630a11e4b5..dce36eca82 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1594,6 +1594,8 @@ class PlaceholderCreateMixin(object): "creator_plugin": creator_plugin } + self._before_instance_create(placeholder) + # compile subset name from variant try: creator_instance = creator_plugin( @@ -1633,6 +1635,11 @@ class PlaceholderCreateMixin(object): pass + def _before_instance_create(self, placeholder): + """Can be overriden. Is called before instance is created.""" + + pass + class LoadPlaceholderItem(PlaceholderItem): """PlaceholderItem for plugin which is loading representations. From 5924dcc1f5d80b532fa30137cf1992b89d71d092 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Dec 2022 14:27:32 +0100 Subject: [PATCH 14/47] nuke: implementing _before_instance_create funtion --- openpype/hosts/nuke/api/workfile_template_builder.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index 5e9e5fcdce..33dcdab749 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -574,6 +574,9 @@ class NukePlaceholderCreatePlugin( placeholder_data["delete"] = False return placeholder_data + def _before_instance_create(self, placeholder): + placeholder.data["nodes_init"] = nuke.allNodes() + def collect_placeholders(self): output = [] scene_placeholders = self._collect_scene_placeholders() From c06315d5d6d2ce567e152f25600b23fc2b7c2b47 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Dec 2022 14:28:03 +0100 Subject: [PATCH 15/47] nuke: fixing logic for creator placeholder plugin processing --- openpype/hosts/nuke/api/workfile_template_builder.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index 33dcdab749..973e15b192 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -76,8 +76,7 @@ class NukePlaceholderPlugin(PlaceholderPlugin): node_knobs = node.knobs() if ( - "builder_type" not in node_knobs - or "is_placeholder" not in node_knobs + "is_placeholder" not in node_knobs or not node.knob("is_placeholder").value() ): continue @@ -756,16 +755,9 @@ class NukePlaceholderCreatePlugin( created_nodes = placeholder.data["last_created"] created_nodes_set = set(created_nodes) - data = {"repre_id": str(placeholder.data["last_repre_id"])} for node in created_nodes: node_knobs = node.knobs() - if "builder_type" not in node_knobs: - # save the id of representation for all imported nodes - imprint(node, data) - node.knob("repre_id").setVisible(False) - refresh_node(node) - continue if ( "is_placeholder" not in node_knobs From c0157e5787a822064238847a4b2a7e5bac71970c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Dec 2022 17:34:47 +0100 Subject: [PATCH 16/47] remove todo --- openpype/hosts/nuke/api/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index 973e15b192..1b81f24e86 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -588,7 +588,7 @@ class NukePlaceholderCreatePlugin( continue placeholder_data = self._parse_placeholder_node_data(node) - # TODO do data validations and maybe updgrades if are invalid + output.append( CreatePlaceholderItem(node_name, placeholder_data, self) ) From f5842d91bd49cc955f49ed53388efd565b84f0a6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Dec 2022 12:18:11 +0100 Subject: [PATCH 17/47] rename variable 'max_len' to 'message_len' --- openpype/widgets/message_window.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/widgets/message_window.py b/openpype/widgets/message_window.py index 94e51f5d4f..8301f5e8f2 100644 --- a/openpype/widgets/message_window.py +++ b/openpype/widgets/message_window.py @@ -105,16 +105,18 @@ class ScrollMessageBox(QtWidgets.QDialog): content_widget = QtWidgets.QWidget(self) scroll_widget.setWidget(content_widget) - max_len = 0 + message_len = 0 content_layout = QtWidgets.QVBoxLayout(content_widget) for message in messages: label_widget = QtWidgets.QLabel(message, content_widget) content_layout.addWidget(label_widget) - max_len = max(max_len, len(message)) + message_len = max(message_len, len(message)) # guess size of scrollable area max_width = QtWidgets.QApplication.desktop().availableGeometry().width - scroll_widget.setMinimumWidth(min(max_width, max_len * 6)) + scroll_widget.setMinimumWidth( + min(max_width, message_len * 6) + ) layout.addWidget(scroll_widget) if not cancelable: # if no specific buttons OK only From a941aabc049ee3ec59ef829dbd58c1abca937868 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Dec 2022 12:21:05 +0100 Subject: [PATCH 18/47] call the width method to get the value --- openpype/widgets/message_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/widgets/message_window.py b/openpype/widgets/message_window.py index 8301f5e8f2..2b186475ee 100644 --- a/openpype/widgets/message_window.py +++ b/openpype/widgets/message_window.py @@ -113,7 +113,7 @@ class ScrollMessageBox(QtWidgets.QDialog): message_len = max(message_len, len(message)) # guess size of scrollable area - max_width = QtWidgets.QApplication.desktop().availableGeometry().width + max_width = QtWidgets.QApplication.desktop().availableGeometry().width() scroll_widget.setMinimumWidth( min(max_width, message_len * 6) ) From f3c13e7669c149ef4b652820a2f72f4553484ea2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Dec 2022 12:26:25 +0100 Subject: [PATCH 19/47] fix too long line --- openpype/widgets/message_window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/widgets/message_window.py b/openpype/widgets/message_window.py index 2b186475ee..a44df2ec8e 100644 --- a/openpype/widgets/message_window.py +++ b/openpype/widgets/message_window.py @@ -113,7 +113,8 @@ class ScrollMessageBox(QtWidgets.QDialog): message_len = max(message_len, len(message)) # guess size of scrollable area - max_width = QtWidgets.QApplication.desktop().availableGeometry().width() + desktop = QtWidgets.QApplication.desktop() + max_width = desktop.availableGeometry().width() scroll_widget.setMinimumWidth( min(max_width, message_len * 6) ) From 7a372d1b1cd481610a044a552e155db62ae8adbb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 3 Jan 2023 17:05:47 +0100 Subject: [PATCH 20/47] Added possibility to mention users or groups --- .../plugins/publish/integrate_slack_api.py | 271 ++++++++++++++---- 1 file changed, 213 insertions(+), 58 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index d94ecb02e4..8d34521194 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -3,6 +3,9 @@ import six import pyblish.api import copy from datetime import datetime +import re +from abc import ABCMeta, abstractmethod +import time from openpype.client import OpenPypeMongoConnection from openpype.lib.plugin_tools import prepare_template_data @@ -33,6 +36,7 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): publish_files = set() message = '' additional_message = instance.data.get("slack_additional_message") + token = instance.data["slack_token"] if additional_message: message = "{} \n".format(additional_message) for message_profile in instance.data["slack_channel_message_profiles"]: @@ -52,18 +56,16 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): project = instance.context.data["anatomyData"]["project"]["code"] for channel in message_profile["channels"]: if six.PY2: - msg_id, file_ids = \ - self._python2_call(instance.data["slack_token"], - channel, - message, - publish_files) + client = SlackPython2Operations(token, self.log) else: - msg_id, file_ids = \ - self._python3_call(instance.data["slack_token"], - channel, - message, - publish_files) + client = SlackPython3Operations(token, self.log) + users, groups = client.get_users_and_groups() + message = self._translate_users(message, users, groups) + + msg_id, file_ids = client.send_message(channel, + message, + publish_files) if not msg_id: return @@ -177,15 +179,211 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): break return published_path - def _python2_call(self, token, channel, message, publish_files): - from slackclient import SlackClient + def _get_user_id(self, users, user_name): + """Returns internal slack id for user name""" + user_id = None + for user in users: + if (not user.get("deleted") and + (user_name.lower() == user["name"].lower() or + user_name.lower() == user["real_name"])): + user_id = user["id"] + break + return user_id + + def _get_group_id(self, groups, group_name): + """Returns internal group id for string name""" + group_id = None + for group in groups: + if (not group.get("date_delete") and + (group_name.lower() == group["name"].lower() or + group_name.lower() == group["handle"])): + group_id = group["id"] + break + return group_id + + def _translate_users(self, message, users, groups): + matches = re.findall("(?".format(slack_id) + else: + slack_id = self._get_group_id(groups, user_name) + if slack_id: + mention = "".format(slack_id) + if mention: + message = message.replace(orig_user, mention) + + return message + + +@six.add_metaclass(ABCMeta) +class AbstractSlackOperations: + + @abstractmethod + def _get_users_list(self): + """Return response with user list, different methods Python 2 vs 3""" + raise NotImplementedError + + @abstractmethod + def _get_usergroups_list(self): + """Return response with user list, different methods Python 2 vs 3""" + raise NotImplementedError + + @abstractmethod + def get_users_and_groups(self): + """Return users and groups, different retry in Python 2 vs 3""" + raise NotImplementedError + + @abstractmethod + def send_message(self, channel, message, publish_files): + """Sends message to channel, different methods in Python 2 vs 3""" + pass + + def _get_users(self): + """Parse users.list response into list of users (dicts)""" + first = True + next_page = None + users = [] + while first or next_page: + response = self._get_users_list() + first = False + next_page = response.get("response_metadata").get("next_cursor") + for user in response.get("members"): + users.append(user) + + return users + + def _get_groups(self): + """Parses usergroups.list response into list of groups (dicts)""" + response = self._get_usergroups_list() + groups = [] + for group in response.get("usergroups"): + groups.append(group) + return groups + + def _enrich_error(self, error_str, channel): + """Enhance known errors with more helpful notations.""" + if 'not_in_channel' in error_str: + # there is no file.write.public scope, app must be explicitly in + # the channel + msg = " - application must added to channel '{}'.".format(channel) + error_str += msg + " Ask Slack admin." + return error_str + + +class SlackPython3Operations(AbstractSlackOperations): + + def __init__(self, token, log): + from slack_sdk import WebClient + + self.client = WebClient(token=token) + self.log = log + + def _get_users_list(self): + return self.client.users_list() + + def _get_usergroups_list(self): + return self.client.usergroups_list() + + def get_users_and_groups(self): + from slack_sdk.errors import SlackApiError + while True: + try: + users = self._get_users() + groups = self._get_groups() + break + except SlackApiError as e: + retry_after = e.response.headers.get("Retry-After") + if retry_after: + print( + "Rate limit hit, sleeping for {}".format(retry_after)) + time.sleep(int(retry_after)) + else: + raise e + + return users, groups + + def send_message(self, channel, message, publish_files): + from slack_sdk.errors import SlackApiError + try: + attachment_str = "\n\n Attachment links: \n" + file_ids = [] + for published_file in publish_files: + response = self.client.files_upload( + file=published_file, + filename=os.path.basename(published_file)) + attachment_str += "\n<{}|{}>".format( + response["file"]["permalink"], + os.path.basename(published_file)) + file_ids.append(response["file"]["id"]) + + if publish_files: + message += attachment_str + + message = self.translate_users(message) + + response = self.client.chat_postMessage( + channel=channel, + text=message + ) + return response.data["ts"], file_ids + except SlackApiError as e: + # # You will get a SlackApiError if "ok" is False + error_str = self._enrich_error(str(e.response["error"]), channel) + self.log.warning("Error happened {}".format(error_str)) + except Exception as e: + error_str = self._enrich_error(str(e), channel) + self.log.warning("Not SlackAPI error", exc_info=True) + + return None, [] + + +class SlackPython2Operations(AbstractSlackOperations): + + def __init__(self, token, log): + from slackclient import SlackClient + + self.client = SlackClient(token=token) + self.log = log + + def _get_users_list(self): + return self.client.api_call("users.list") + + def _get_usergroups_list(self): + return self.client.api_call("usergroups.list") + + def get_users_and_groups(self): + while True: + try: + users = self._get_users() + groups = self._get_groups() + break + except Exception as e: + retry_after = e.response.headers.get("Retry-After") + if retry_after: + print( + "Rate limit hit, sleeping for {}".format(retry_after)) + time.sleep(int(retry_after)) + else: + raise e + + return users, groups + + def send_message(self, channel, message, publish_files): try: - client = SlackClient(token) attachment_str = "\n\n Attachment links: \n" file_ids = [] for p_file in publish_files: with open(p_file, 'rb') as pf: - response = client.api_call( + response = self.client.api_call( "files.upload", file=pf, channel=channel, @@ -206,7 +404,7 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): if publish_files: message += attachment_str - response = client.api_call( + response = self.client.api_call( "chat.postMessage", channel=channel, text=message @@ -223,46 +421,3 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): self.log.warning("Error happened: {}".format(error_str)) return None, [] - - def _python3_call(self, token, channel, message, publish_files): - from slack_sdk import WebClient - from slack_sdk.errors import SlackApiError - try: - client = WebClient(token=token) - attachment_str = "\n\n Attachment links: \n" - file_ids = [] - for published_file in publish_files: - response = client.files_upload( - file=published_file, - filename=os.path.basename(published_file)) - attachment_str += "\n<{}|{}>".format( - response["file"]["permalink"], - os.path.basename(published_file)) - file_ids.append(response["file"]["id"]) - - if publish_files: - message += attachment_str - - response = client.chat_postMessage( - channel=channel, - text=message - ) - return response.data["ts"], file_ids - except SlackApiError as e: - # You will get a SlackApiError if "ok" is False - error_str = self._enrich_error(str(e.response["error"]), channel) - self.log.warning("Error happened {}".format(error_str)) - except Exception as e: - error_str = self._enrich_error(str(e), channel) - self.log.warning("Not SlackAPI error", exc_info=True) - - return None, [] - - def _enrich_error(self, error_str, channel): - """Enhance known errors with more helpful notations.""" - if 'not_in_channel' in error_str: - # there is no file.write.public scope, app must be explicitly in - # the channel - msg = " - application must added to channel '{}'.".format(channel) - error_str += msg + " Ask Slack admin." - return error_str From 1ee50975c0e61acac51f7f76b905a420e95487c6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 3 Jan 2023 17:14:41 +0100 Subject: [PATCH 21/47] Fix wrong position of method --- .../plugins/publish/integrate_slack_api.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index a010d08a82..fc5342177d 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -228,6 +228,25 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): return message + def _escape_missing_keys(self, message, fill_data): + """Double escapes placeholder which are missing in 'fill_data'""" + placeholder_keys = re.findall("\{([^}]+)\}", message) + + fill_keys = [] + for key, value in fill_data.items(): + fill_keys.append(key) + if isinstance(value, dict): + for child_key in value.keys(): + fill_keys.append("{}[{}]".format(key, child_key)) + + not_matched = set(placeholder_keys) - set(fill_keys) + + for not_matched_item in not_matched: + message = message.replace("{}".format(not_matched_item), + "{{{}}}".format(not_matched_item)) + + return message + @six.add_metaclass(ABCMeta) class AbstractSlackOperations: @@ -283,25 +302,6 @@ class AbstractSlackOperations: error_str += msg + " Ask Slack admin." return error_str - def _escape_missing_keys(self, message, fill_data): - """Double escapes placeholder which are missing in 'fill_data'""" - placeholder_keys = re.findall("\{([^}]+)\}", message) - - fill_keys = [] - for key, value in fill_data.items(): - fill_keys.append(key) - if isinstance(value, dict): - for child_key in value.keys(): - fill_keys.append("{}[{}]".format(key, child_key)) - - not_matched = set(placeholder_keys) - set(fill_keys) - - for not_matched_item in not_matched: - message = message.replace("{}".format(not_matched_item), - "{{{}}}".format(not_matched_item)) - - return message - class SlackPython3Operations(AbstractSlackOperations): From f80fe3fb938488009974cd931b285ea8564d7b2b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 3 Jan 2023 17:14:57 +0100 Subject: [PATCH 22/47] Fix obsolete call of method --- openpype/modules/slack/plugins/publish/integrate_slack_api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index fc5342177d..803a07f5d2 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -352,8 +352,6 @@ class SlackPython3Operations(AbstractSlackOperations): if publish_files: message += attachment_str - message = self.translate_users(message) - response = self.client.chat_postMessage( channel=channel, text=message From 394c678299f26967ba64b87a7c8684c1d0419191 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 3 Jan 2023 17:19:50 +0100 Subject: [PATCH 23/47] Do not throw exception if user or group list error Skip notification, publish shouldn't fail because of this. --- .../modules/slack/plugins/publish/integrate_slack_api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 803a07f5d2..f18b927c98 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -331,7 +331,9 @@ class SlackPython3Operations(AbstractSlackOperations): "Rate limit hit, sleeping for {}".format(retry_after)) time.sleep(int(retry_after)) else: - raise e + self.log.warning("Cannot pull user info, " + "mentions won't work", exc_info=True) + return [], [] return users, groups @@ -395,7 +397,9 @@ class SlackPython2Operations(AbstractSlackOperations): "Rate limit hit, sleeping for {}".format(retry_after)) time.sleep(int(retry_after)) else: - raise e + self.log.warning("Cannot pull user info, " + "mentions won't work", exc_info=True) + return [], [] return users, groups From 61ef7479e8bbb842378229e87b2187924a368c7a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 3 Jan 2023 17:21:14 +0100 Subject: [PATCH 24/47] =?UTF-8?q?=F0=9F=94=A7=20pass=20mongo=20url=20as=20?= =?UTF-8?q?default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openpype/settings/defaults/project_settings/deadline.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 6e1c0f3540..527f5c0d24 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -2,7 +2,7 @@ "deadline_servers": [], "publish": { "CollectDefaultDeadlineServer": { - "pass_mongo_url": false + "pass_mongo_url": true }, "CollectDeadlinePools": { "primary_pool": "", From 68fe82323883c8035f3831820681174980649802 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 3 Jan 2023 17:53:00 +0100 Subject: [PATCH 25/47] Fix resolving of user_id Display name or real_name could be used also. --- .../slack/plugins/publish/integrate_slack_api.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index f18b927c98..577ead9667 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -187,10 +187,13 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): def _get_user_id(self, users, user_name): """Returns internal slack id for user name""" user_id = None + user_name_lower = user_name.lower() for user in users: if (not user.get("deleted") and - (user_name.lower() == user["name"].lower() or - user_name.lower() == user["real_name"])): + (user_name_lower == user["name"].lower() or + # bots dont have display_name + user_name_lower == user.get("display_name", '').lower() or + user_name_lower == user.get("real_name", '').lower())): user_id = user["id"] break return user_id @@ -208,8 +211,9 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): def _translate_users(self, message, users, groups): matches = re.findall("(? Date: Tue, 3 Jan 2023 17:58:28 +0100 Subject: [PATCH 26/47] Updated documentation --- website/docs/module_slack.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/docs/module_slack.md b/website/docs/module_slack.md index 2bfd7cb562..1999912fdc 100644 --- a/website/docs/module_slack.md +++ b/website/docs/module_slack.md @@ -100,6 +100,10 @@ them to add additional message (notification for specific users for example, art Additional message will be sent only if at least one profile, eg. one target channel is configured. All available template keys (see higher) could be used here as a placeholder too. +#### User or group notifications +Message template or dynamic data could contain user or group notification, it must be in format @artist.name, '@John Doe' or "@admin group" for display name containing space. +If value prefixed with @ is not resolved and Slack user is not found, message will contain same value (not translated by Slack into link and proper mention.) + #### Message retention Currently no purging of old messages is implemented in Openpype. Admins of Slack should set their own retention of messages and files per channel. (see https://slack.com/help/articles/203457187-Customize-message-and-file-retention-policies) From b2e8ea6fb80faeef67cbae6db212422c43cb0b58 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 4 Jan 2023 11:36:12 +0100 Subject: [PATCH 27/47] Hound --- .../modules/slack/plugins/publish/integrate_slack_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 577ead9667..97182ffd9b 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -4,7 +4,6 @@ import six import pyblish.api import copy from datetime import datetime -import re from abc import ABCMeta, abstractmethod import time @@ -210,8 +209,9 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): return group_id def _translate_users(self, message, users, groups): - matches = re.findall("(? format.""" + matches = re.findall(r"(? Date: Wed, 4 Jan 2023 12:44:07 +0100 Subject: [PATCH 28/47] Fix - search pattern Updated to use user profile --- .../modules/slack/plugins/publish/integrate_slack_api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 97182ffd9b..02197a6d01 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -191,8 +191,10 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): if (not user.get("deleted") and (user_name_lower == user["name"].lower() or # bots dont have display_name - user_name_lower == user.get("display_name", '').lower() or - user_name_lower == user.get("real_name", '').lower())): + user_name_lower == user["profile"].get("display_name", + '').lower() or + user_name_lower == user["profile"].get("real_name", + '').lower())): user_id = user["id"] break return user_id @@ -210,7 +212,7 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): def _translate_users(self, message, users, groups): """Replace all occurences of @mentions with proper <@name> format.""" - matches = re.findall(r"(? Date: Wed, 4 Jan 2023 12:51:47 +0100 Subject: [PATCH 29/47] Fix - cannot pull response from ordinary exception --- .../slack/plugins/publish/integrate_slack_api.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 02197a6d01..bb5cd40936 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -397,15 +397,9 @@ class SlackPython2Operations(AbstractSlackOperations): groups = self._get_groups() break except Exception as e: - retry_after = e.response.headers.get("Retry-After") - if retry_after: - print( - "Rate limit hit, sleeping for {}".format(retry_after)) - time.sleep(int(retry_after)) - else: - self.log.warning("Cannot pull user info, " - "mentions won't work", exc_info=True) - return [], [] + self.log.warning("Cannot pull user info, " + "mentions won't work", exc_info=True) + return [], [] return users, groups From dff87d4e1cfabafb4ec40c46a1cbc48c851f661f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 4 Jan 2023 12:52:32 +0100 Subject: [PATCH 30/47] Hound --- openpype/modules/slack/plugins/publish/integrate_slack_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index bb5cd40936..21069e0b13 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -396,7 +396,7 @@ class SlackPython2Operations(AbstractSlackOperations): users = self._get_users() groups = self._get_groups() break - except Exception as e: + except Exception: self.log.warning("Cannot pull user info, " "mentions won't work", exc_info=True) return [], [] From 40cf2956fd968c8ffaf47c584c69705a852e4bc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 4 Jan 2023 14:26:15 +0100 Subject: [PATCH 31/47] Update openpype/pipeline/workfile/workfile_template_builder.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/pipeline/workfile/workfile_template_builder.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index dce36eca82..24b0cc81f1 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1504,7 +1504,6 @@ class PlaceholderCreateMixin(object): """ creators_by_name = self.builder.get_creators_by_name() - print(creators_by_name) creator_items = [ (creator_name, creator.label or creator_name) From e69f3539eaeac23d0663e71f83c9d463cbc50699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 4 Jan 2023 14:26:26 +0100 Subject: [PATCH 32/47] Update openpype/pipeline/workfile/workfile_template_builder.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/pipeline/workfile/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 24b0cc81f1..a834ca0e21 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -675,7 +675,7 @@ class AbstractTemplateBuilder(object): # switch to remove placeholders after they are used placeholder_keep = profile.get("placeholder_keep") # backward compatibility, since default is True - if placeholder_keep is not False: + if placeholder_keep is None: placeholder_keep = True if not path: From b01322645ae14e74b09d4125fcac5a8160d76c57 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 4 Jan 2023 14:44:04 +0100 Subject: [PATCH 33/47] Correctly repair frame range with handle attributes if `handleStart` and `handleEnd` available on instance --- .../plugins/publish/validate_frame_range.py | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_frame_range.py b/openpype/hosts/maya/plugins/publish/validate_frame_range.py index 5e50ae72cd..dec2f00700 100644 --- a/openpype/hosts/maya/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/maya/plugins/publish/validate_frame_range.py @@ -92,10 +92,31 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): """ Repair instance container to match asset data. """ - cmds.setAttr( - "{}.frameStart".format(instance.data["name"]), - instance.context.data.get("frameStartHandle")) - cmds.setAttr( - "{}.frameEnd".format(instance.data["name"]), - instance.context.data.get("frameEndHandle")) + node = instance.data["name"] + context = instance.context + + frame_start_handle = int(context.data.get("frameStartHandle")) + frame_end_handle = int(context.data.get("frameEndHandle")) + handle_start = int(context.data.get("handleStart")) + handle_end = int(context.data.get("handleEnd")) + frame_start = int(context.data.get("frameStart")) + frame_end = int(context.data.get("frameEnd")) + + # Start + if cmds.attributeQuery("handleStart", node=node, exists=True): + cmds.setAttr("{}.handleStart".format(node), handle_start) + cmds.setAttr("{}.frameStart".format(node), frame_start) + else: + # Include start handle in frame start if no separate handleStart + # attribute exists on the node + cmds.setAttr("{}.frameStart".format(node), frame_start_handle) + + # End + if cmds.attributeQuery("handleEnd", node=node, exists=True): + cmds.setAttr("{}.handleEnd".format(node), handle_end) + cmds.setAttr("{}.frameEnd".format(node), frame_end) + else: + # Include end handle in frame end if no separate handleEnd + # attribute exists on the node + cmds.setAttr("{}.frameEnd".format(node), frame_end_handle) From 0a4ed0988c0a359bbec6b6929d9073215af3cdea Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 4 Jan 2023 14:45:00 +0100 Subject: [PATCH 34/47] Do not force instance handleStart and handleEnd to zero if not `handles` in data --- openpype/hosts/maya/plugins/publish/collect_instances.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_instances.py b/openpype/hosts/maya/plugins/publish/collect_instances.py index ad1f794680..75bc935143 100644 --- a/openpype/hosts/maya/plugins/publish/collect_instances.py +++ b/openpype/hosts/maya/plugins/publish/collect_instances.py @@ -174,9 +174,6 @@ class CollectInstances(pyblish.api.ContextPlugin): if "handles" in data: data["handleStart"] = data["handles"] data["handleEnd"] = data["handles"] - else: - data["handleStart"] = 0 - data["handleEnd"] = 0 data["frameStartHandle"] = data["frameStart"] - data["handleStart"] # noqa: E501 data["frameEndHandle"] = data["frameEnd"] + data["handleEnd"] # noqa: E501 From e44f585aa63c32ebb6913426626fdb4c3fd0a008 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 4 Jan 2023 15:05:36 +0100 Subject: [PATCH 35/47] OP-4490 - safer resolving if site is active --- openpype/modules/sync_server/providers/dropbox.py | 2 +- openpype/modules/sync_server/providers/gdrive.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/sync_server/providers/dropbox.py b/openpype/modules/sync_server/providers/dropbox.py index 3515aee93f..a517e7d847 100644 --- a/openpype/modules/sync_server/providers/dropbox.py +++ b/openpype/modules/sync_server/providers/dropbox.py @@ -165,7 +165,7 @@ class DropboxHandler(AbstractProvider): Returns: (boolean) """ - return self.presets["enabled"] and self.dbx is not None + return self.presets.get("enabled") and self.dbx is not None @classmethod def get_configurable_items(cls): diff --git a/openpype/modules/sync_server/providers/gdrive.py b/openpype/modules/sync_server/providers/gdrive.py index 297a5c9fec..4e24fe41d2 100644 --- a/openpype/modules/sync_server/providers/gdrive.py +++ b/openpype/modules/sync_server/providers/gdrive.py @@ -119,7 +119,7 @@ class GDriveHandler(AbstractProvider): Returns: (boolean) """ - return self.presets["enabled"] and self.service is not None + return self.presets.get("enabled") and self.service is not None @classmethod def get_system_settings_schema(cls): From 8527554c2d3bafd1a7eb796219fe7cefebcf749b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 4 Jan 2023 15:07:36 +0100 Subject: [PATCH 36/47] OP-4490 - fixed unnecessary checks Configured sites were checked all the time even if they weren't used. Now it checks only sites that are set for project. --- openpype/modules/sync_server/sync_server.py | 67 ++++++--------------- 1 file changed, 17 insertions(+), 50 deletions(-) diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index d0a40a60ff..d1ca69a31c 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -169,7 +169,7 @@ def resolve_paths(module, file_path, project_name, return local_file_path, remote_file_path -def site_is_working(module, project_name, site_name): +def _site_is_working(module, project_name, site_name, site_config): """ Confirm that 'site_name' is configured correctly for 'project_name'. @@ -179,54 +179,17 @@ def site_is_working(module, project_name, site_name): module (SyncServerModule) project_name(string): site_name(string): + site_config (dict): configuration for site from Settings Returns (bool) """ - if _get_configured_sites(module, project_name).get(site_name): - return True - return False + provider = module.get_provider_for_site(site=site_name) + handler = lib.factory.get_provider(provider, + project_name, + site_name, + presets=site_config) - -def _get_configured_sites(module, project_name): - """ - Loops through settings and looks for configured sites and checks - its handlers for particular 'project_name'. - - Args: - project_setting(dict): dictionary from Settings - only_project_name(string, optional): only interested in - particular project - Returns: - (dict of dict) - {'ProjectA': {'studio':True, 'gdrive':False}} - """ - settings = module.get_sync_project_setting(project_name) - return _get_configured_sites_from_setting(module, project_name, settings) - - -def _get_configured_sites_from_setting(module, project_name, project_setting): - if not project_setting.get("enabled"): - return {} - - initiated_handlers = {} - configured_sites = {} - all_sites = module._get_default_site_configs() - all_sites.update(project_setting.get("sites")) - for site_name, config in all_sites.items(): - provider = module.get_provider_for_site(site=site_name) - handler = initiated_handlers.get((provider, site_name)) - if not handler: - handler = lib.factory.get_provider(provider, - project_name, - site_name, - presets=config) - initiated_handlers[(provider, site_name)] = \ - handler - - if handler.is_active(): - configured_sites[site_name] = True - - return configured_sites + return handler.is_active() class SyncServerThread(threading.Thread): @@ -288,7 +251,8 @@ class SyncServerThread(threading.Thread): for project_name in enabled_projects: preset = self.module.sync_project_settings[project_name] - local_site, remote_site = self._working_sites(project_name) + local_site, remote_site = self._working_sites(project_name, + preset) if not all([local_site, remote_site]): continue @@ -464,7 +428,7 @@ class SyncServerThread(threading.Thread): self.timer.cancel() self.timer = None - def _working_sites(self, project_name): + def _working_sites(self, project_name, sync_config): if self.module.is_project_paused(project_name): self.log.debug("Both sites same, skipping") return None, None @@ -476,9 +440,12 @@ class SyncServerThread(threading.Thread): local_site, remote_site)) return None, None - configured_sites = _get_configured_sites(self.module, project_name) - if not all([local_site in configured_sites, - remote_site in configured_sites]): + local_site_config = sync_config.get('sites')[local_site] + remote_site_config = sync_config.get('sites')[remote_site] + if not all([_site_is_working(self.module, project_name, local_site, + local_site_config), + _site_is_working(self.module, project_name, remote_site, + remote_site_config)]): self.log.debug( "Some of the sites {} - {} is not working properly".format( local_site, remote_site From 307a10e123ad0c177248b144f5a2aadc2c503491 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 4 Jan 2023 15:35:43 +0100 Subject: [PATCH 37/47] Implement validate frame range repair for Render Layers - fix #3302 --- .../plugins/publish/validate_frame_range.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_frame_range.py b/openpype/hosts/maya/plugins/publish/validate_frame_range.py index dec2f00700..d86925184e 100644 --- a/openpype/hosts/maya/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/maya/plugins/publish/validate_frame_range.py @@ -5,6 +5,11 @@ from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, ) +from openpype.hosts.maya.api.lib_rendersetup import ( + get_attr_overrides, + get_attr_in_layer, +) +from maya.app.renderSetup.model.override import AbsOverride class ValidateFrameRange(pyblish.api.InstancePlugin): @@ -93,6 +98,11 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): Repair instance container to match asset data. """ + if "renderlayer" in instance.data.get("families"): + # Special behavior for renderlayers + cls.repair_renderlayer(instance) + return + node = instance.data["name"] context = instance.context @@ -120,3 +130,53 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): # Include end handle in frame end if no separate handleEnd # attribute exists on the node cmds.setAttr("{}.frameEnd".format(node), frame_end_handle) + + @classmethod + def repair_renderlayer(cls, instance): + """Apply frame range in render settings""" + + layer = instance.data["setMembers"] + context = instance.context + + start_attr = "defaultRenderGlobals.startFrame" + end_attr = "defaultRenderGlobals.endFrame" + + frame_start_handle = int(context.data.get("frameStartHandle")) + frame_end_handle = int(context.data.get("frameEndHandle")) + + cls._set_attr_in_layer(start_attr, layer, frame_start_handle) + cls._set_attr_in_layer(end_attr, layer, frame_end_handle) + + @classmethod + def _set_attr_in_layer(cls, node_attr, layer, value): + + if get_attr_in_layer(node_attr, layer=layer) == value: + # Already ok. This can happen if you have multiple renderlayers + # validated and there are no frame range overrides. The first + # layer's repair would have fixed the global value already + return + + overrides = list(get_attr_overrides(node_attr, layer=layer)) + if overrides: + # We set the last absolute override if it is an absolute override + # otherwise we'll add an Absolute override + last_override = overrides[-1][1] + if not isinstance(last_override, AbsOverride): + collection = last_override.parent() + node, attr = node_attr.split(".", 1) + last_override = collection.createAbsoluteOverride(node, attr) + + cls.log.debug("Setting {attr} absolute override in " + "layer '{layer}': {value}".format(layer=layer, + attr=node_attr, + value=value)) + cmds.setAttr(last_override.name() + ".attrValue", value) + + else: + # Set the attribute directly + # (Note that this will set the global attribute) + cls.log.debug("Setting global {attr}: {value}".format( + attr=node_attr, + value=value + )) + cmds.setAttr(node_attr, value) From e353095c24e219914f41b323feba9214f85c38fb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 4 Jan 2023 15:36:19 +0100 Subject: [PATCH 38/47] Only apply deadline attributes when Deadline is enabled (fixes Create Render with Deadline module disabled) --- .../maya/plugins/create/create_render.py | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index a3e1272652..8375149442 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -72,15 +72,19 @@ class CreateRender(plugin.Creator): def __init__(self, *args, **kwargs): """Constructor.""" super(CreateRender, self).__init__(*args, **kwargs) - deadline_settings = get_system_settings()["modules"]["deadline"] - if not deadline_settings["enabled"]: - self.deadline_servers = {} - return + + # Defaults self._project_settings = get_project_settings( legacy_io.Session["AVALON_PROJECT"]) if self._project_settings["maya"]["RenderSettings"]["apply_render_settings"]: # noqa lib_rendersettings.RenderSettings().set_default_renderer_settings() + + # Deadline-only manager = ModulesManager() + deadline_settings = get_system_settings()["modules"]["deadline"] + if not deadline_settings["enabled"]: + self.deadline_servers = {} + return self.deadline_module = manager.modules_by_name["deadline"] try: default_servers = deadline_settings["deadline_urls"] @@ -193,8 +197,6 @@ class CreateRender(plugin.Creator): pool_names = [] default_priority = 50 - self.server_aliases = list(self.deadline_servers.keys()) - self.data["deadlineServers"] = self.server_aliases self.data["suspendPublishJob"] = False self.data["review"] = True self.data["extendFrames"] = False @@ -233,6 +235,9 @@ class CreateRender(plugin.Creator): raise RuntimeError("Both Deadline and Muster are enabled") if deadline_enabled: + self.server_aliases = list(self.deadline_servers.keys()) + self.data["deadlineServers"] = self.server_aliases + try: deadline_url = self.deadline_servers["default"] except KeyError: @@ -254,6 +259,19 @@ class CreateRender(plugin.Creator): default_priority) self.data["tile_priority"] = tile_priority + pool_setting = (self._project_settings["deadline"] + ["publish"] + ["CollectDeadlinePools"]) + primary_pool = pool_setting["primary_pool"] + self.data["primaryPool"] = self._set_default_pool(pool_names, + primary_pool) + # We add a string "-" to allow the user to not + # set any secondary pools + pool_names = ["-"] + pool_names + secondary_pool = pool_setting["secondary_pool"] + self.data["secondaryPool"] = self._set_default_pool(pool_names, + secondary_pool) + if muster_enabled: self.log.info(">>> Loading Muster credentials ...") self._load_credentials() @@ -273,18 +291,6 @@ class CreateRender(plugin.Creator): self.log.info(" - pool: {}".format(pool["name"])) pool_names.append(pool["name"]) - pool_setting = (self._project_settings["deadline"] - ["publish"] - ["CollectDeadlinePools"]) - primary_pool = pool_setting["primary_pool"] - self.data["primaryPool"] = self._set_default_pool(pool_names, - primary_pool) - # We add a string "-" to allow the user to not - # set any secondary pools - pool_names = ["-"] + pool_names - secondary_pool = pool_setting["secondary_pool"] - self.data["secondaryPool"] = self._set_default_pool(pool_names, - secondary_pool) self.options = {"useSelection": False} # Force no content def _set_default_pool(self, pool_names, pool_value): From d3f09c075badd52f3faa9d4fe41678dd1abf1d9b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 4 Jan 2023 15:39:13 +0100 Subject: [PATCH 39/47] OP-4490 - Hound --- openpype/modules/sync_server/sync_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index d1ca69a31c..85b0774e90 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -443,9 +443,9 @@ class SyncServerThread(threading.Thread): local_site_config = sync_config.get('sites')[local_site] remote_site_config = sync_config.get('sites')[remote_site] if not all([_site_is_working(self.module, project_name, local_site, - local_site_config), + local_site_config), _site_is_working(self.module, project_name, remote_site, - remote_site_config)]): + remote_site_config)]): self.log.debug( "Some of the sites {} - {} is not working properly".format( local_site, remote_site From 5c6d86e06b84c464d0bafb640d5ef428f3d60650 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 Jan 2023 10:35:14 +0100 Subject: [PATCH 40/47] maya: fix typo in template builder --- openpype/hosts/maya/api/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index 1d3f1cf568..3416c98793 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -240,7 +240,7 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): cmds.setAttr(node + ".hiddenInOutliner", True) def load_succeed(self, placeholder, container): - self._parent_in_hierarhchy(placeholder, container) + self._parent_in_hierarchy(placeholder, container) def _parent_in_hierarchy(self, placeholder, container): """Parent loaded container to placeholder's parent. From 20b2b50bac741d82b454b207d83c1ca5bda849a3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 Jan 2023 10:35:54 +0100 Subject: [PATCH 41/47] global: adding project anatomy data for formating --- openpype/pipeline/workfile/workfile_template_builder.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index a834ca0e21..390a5759fc 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -691,7 +691,14 @@ class AbstractTemplateBuilder(object): key: value for key, value in os.environ.items() } + fill_data["root"] = anatomy.roots + fill_data["project"] = { + "name": project_name, + "code": anatomy["attributes"]["code"] + } + + result = StringTemplate.format_template(path, fill_data) if result.solved: path = result.normalized() From 673e7a735928408dcc7eae463aba62af2a53744a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 6 Jan 2023 10:38:36 +0100 Subject: [PATCH 42/47] Update openpype/pipeline/workfile/workfile_template_builder.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/pipeline/workfile/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 390a5759fc..58f152591f 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1517,7 +1517,7 @@ class PlaceholderCreateMixin(object): for creator_name, creator in creators_by_name.items() ] - creator_items = list(sorted(creator_items, key=lambda i: i[1])) + creator_items.sort(key=lambda i: i[1]) options = options or {} return [ attribute_definitions.UISeparatorDef(), From d6004c26462e3f0400674ce1fbc3b6936c46b5cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 6 Jan 2023 10:39:43 +0100 Subject: [PATCH 43/47] Update openpype/pipeline/workfile/workfile_template_builder.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/pipeline/workfile/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 58f152591f..4fa45cdf30 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1574,7 +1574,7 @@ class PlaceholderCreateMixin(object): creator_name = placeholder.data["creator"] create_variant = placeholder.data["create_variant"] - creator_plugin = get_legacy_creator_by_name(creator_name) + creator_plugin = self.builder.get_creators_by_name()[creator_name] # create subset name project_name = legacy_io.Session["AVALON_PROJECT"] From c071140a6fcc8525b78f7f6b3bce8303bc011505 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 Jan 2023 10:54:06 +0100 Subject: [PATCH 44/47] PR comments --- .../workfile/workfile_template_builder.py | 25 +++++++++++-------- .../schema_templated_workfile_build.json | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 4fa45cdf30..e3821bb4d7 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -43,7 +43,6 @@ from openpype.pipeline.load import ( load_with_repre_context, ) from openpype.pipeline.create import ( - get_legacy_creator_by_name, discover_legacy_creator_plugins ) @@ -439,7 +438,7 @@ class AbstractTemplateBuilder(object): template_path = template_preset["path"] if keep_placeholders is None: - keep_placeholders = template_preset["placeholder_keep"] + keep_placeholders = template_preset["keep_placeholder"] self.import_template(template_path) self.populate_scene_placeholders( @@ -673,10 +672,10 @@ class AbstractTemplateBuilder(object): path = profile["path"] # switch to remove placeholders after they are used - placeholder_keep = profile.get("placeholder_keep") + keep_placeholder = profile.get("keep_placeholder") # backward compatibility, since default is True - if placeholder_keep is None: - placeholder_keep = True + if keep_placeholder is None: + keep_placeholder = True if not path: raise TemplateLoadFailed(( @@ -707,7 +706,7 @@ class AbstractTemplateBuilder(object): self.log.info("Found template at: '{}'".format(path)) return { "path": path, - "placeholder_keep": placeholder_keep + "keep_placeholder": keep_placeholder } solved_path = None @@ -736,7 +735,7 @@ class AbstractTemplateBuilder(object): return { "path": solved_path, - "placeholder_keep": placeholder_keep + "keep_placeholder": keep_placeholder } @@ -991,7 +990,7 @@ class PlaceholderItem(object): def __init__(self, scene_identifier, data, plugin): self._log = None - self.name = scene_identifier + self._scene_identifier = scene_identifier self._data = data self._plugin = plugin @@ -1056,7 +1055,13 @@ class PlaceholderItem(object): return self._log def __repr__(self): - return "< {} {} >".format(self.__class__.__name__, self.name) + name = None + if hasattr("name", self): + name = self.name + if hasattr("_scene_identifier ", self): + name = self._scene_identifier + + return "< {} {} >".format(self.__class__.__name__, name) @property def order(self): @@ -1069,7 +1074,7 @@ class PlaceholderItem(object): @property def scene_identifier(self): - return self.name + return self._scene_identifier @property def finished(self): diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json index 1826734291..b244460bbf 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json @@ -30,7 +30,7 @@ "multipath": false }, { - "key": "placeholder_keep", + "key": "keep_placeholder", "label": "Keep placeholders", "type": "boolean", "default": true From cd2324f07e48e03b36a99918d3110e6d505757f4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 6 Jan 2023 15:42:47 +0100 Subject: [PATCH 45/47] fix how host ip is received --- .../ftrack/ftrack_server/event_server_cli.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/ftrack_server/event_server_cli.py b/openpype/modules/ftrack/ftrack_server/event_server_cli.py index 20c5ab24a8..9adc784224 100644 --- a/openpype/modules/ftrack/ftrack_server/event_server_cli.py +++ b/openpype/modules/ftrack/ftrack_server/event_server_cli.py @@ -169,6 +169,22 @@ def legacy_server(ftrack_url): time.sleep(1) +def get_host_ip(): + host_name = socket.gethostname() + try: + return socket.gethostbyname(host_name) + except Exception: + pass + + try: + import ipaddress + return socket.gethostbyname(str(ipaddress.ip_address(8888))) + + except Exception: + pass + return None + + def main_loop(ftrack_url): """ This is main loop of event handling. @@ -245,11 +261,13 @@ def main_loop(ftrack_url): ) host_name = socket.gethostname() + host_ip = get_host_ip() + main_info = [ ["created_at", datetime.datetime.now().strftime("%Y.%m.%d %H:%M:%S")], ["Username", getpass.getuser()], ["Host Name", host_name], - ["Host IP", socket.gethostbyname(host_name)], + ["Host IP", host_ip or "N/A"], ["OpenPype executable", get_openpype_execute_args()[-1]], ["OpenPype version", get_openpype_version() or "N/A"], ["OpenPype build version", get_build_version() or "N/A"] From e1edb76f731097f9887c096db0fdcb05aaed0616 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 6 Jan 2023 17:48:05 +0100 Subject: [PATCH 46/47] use 'get_host_ip' on more places --- .../action_where_run_ask.py | 4 ++-- .../ftrack/ftrack_server/event_server_cli.py | 17 +---------------- openpype/modules/ftrack/ftrack_server/lib.py | 19 ++++++++++++++++++- .../ftrack/scripts/sub_event_status.py | 7 ++++--- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_where_run_ask.py b/openpype/modules/ftrack/event_handlers_user/action_where_run_ask.py index 0d69913996..65d1b42d82 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_where_run_ask.py +++ b/openpype/modules/ftrack/event_handlers_user/action_where_run_ask.py @@ -3,6 +3,7 @@ import socket import getpass from openpype_modules.ftrack.lib import BaseAction +from openpype_modules.ftrack.ftrack_server.lib import get_host_ip class ActionWhereIRun(BaseAction): @@ -53,8 +54,7 @@ class ActionWhereIRun(BaseAction): try: host_name = socket.gethostname() msgs["Hostname"] = host_name - host_ip = socket.gethostbyname(host_name) - msgs["IP"] = host_ip + msgs["IP"] = get_host_ip() or "N/A" except Exception: pass diff --git a/openpype/modules/ftrack/ftrack_server/event_server_cli.py b/openpype/modules/ftrack/ftrack_server/event_server_cli.py index 9adc784224..25ebad6658 100644 --- a/openpype/modules/ftrack/ftrack_server/event_server_cli.py +++ b/openpype/modules/ftrack/ftrack_server/event_server_cli.py @@ -26,6 +26,7 @@ from openpype_modules.ftrack import ( ) from openpype_modules.ftrack.lib import credentials from openpype_modules.ftrack.ftrack_server import socket_thread +from openpype_modules.ftrack.ftrack_server.lib import get_host_ip class MongoPermissionsError(Exception): @@ -169,22 +170,6 @@ def legacy_server(ftrack_url): time.sleep(1) -def get_host_ip(): - host_name = socket.gethostname() - try: - return socket.gethostbyname(host_name) - except Exception: - pass - - try: - import ipaddress - return socket.gethostbyname(str(ipaddress.ip_address(8888))) - - except Exception: - pass - return None - - def main_loop(ftrack_url): """ This is main loop of event handling. diff --git a/openpype/modules/ftrack/ftrack_server/lib.py b/openpype/modules/ftrack/ftrack_server/lib.py index c8143f739c..61d3bfa259 100644 --- a/openpype/modules/ftrack/ftrack_server/lib.py +++ b/openpype/modules/ftrack/ftrack_server/lib.py @@ -9,8 +9,9 @@ import time import queue import collections import appdirs -import pymongo +import socket +import pymongo import requests import ftrack_api import ftrack_api.session @@ -32,6 +33,22 @@ TOPIC_STATUS_SERVER = "openpype.event.server.status" TOPIC_STATUS_SERVER_RESULT = "openpype.event.server.status.result" +def get_host_ip(): + host_name = socket.gethostname() + try: + return socket.gethostbyname(host_name) + except Exception: + pass + + try: + import ipaddress + return socket.gethostbyname(str(ipaddress.ip_address(8888))) + + except Exception: + pass + return None + + class SocketBaseEventHub(ftrack_api.event.hub.EventHub): hearbeat_msg = b"hearbeat" diff --git a/openpype/modules/ftrack/scripts/sub_event_status.py b/openpype/modules/ftrack/scripts/sub_event_status.py index eb3f63c04b..dc5836e7f2 100644 --- a/openpype/modules/ftrack/scripts/sub_event_status.py +++ b/openpype/modules/ftrack/scripts/sub_event_status.py @@ -15,7 +15,8 @@ from openpype_modules.ftrack.ftrack_server.lib import ( SocketSession, StatusEventHub, TOPIC_STATUS_SERVER, - TOPIC_STATUS_SERVER_RESULT + TOPIC_STATUS_SERVER_RESULT, + get_host_ip ) from openpype.lib import ( Logger, @@ -29,10 +30,10 @@ log = Logger.get_logger("Event storer") action_identifier = ( "event.server.status" + os.environ["FTRACK_EVENT_SUB_ID"] ) -host_ip = socket.gethostbyname(socket.gethostname()) +host_ip = get_host_ip() action_data = { "label": "OpenPype Admin", - "variant": "- Event server Status ({})".format(host_ip), + "variant": "- Event server Status ({})".format(host_ip or "IP N/A"), "description": "Get Infromation about event server", "actionIdentifier": action_identifier } From 1c530c0cb4c35353b7b697905d79c5628c705ff3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 6 Jan 2023 18:09:17 +0100 Subject: [PATCH 47/47] skip ipaddress way to receive ip address --- openpype/modules/ftrack/ftrack_server/lib.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/modules/ftrack/ftrack_server/lib.py b/openpype/modules/ftrack/ftrack_server/lib.py index 61d3bfa259..eb64063fab 100644 --- a/openpype/modules/ftrack/ftrack_server/lib.py +++ b/openpype/modules/ftrack/ftrack_server/lib.py @@ -40,12 +40,6 @@ def get_host_ip(): except Exception: pass - try: - import ipaddress - return socket.gethostbyname(str(ipaddress.ip_address(8888))) - - except Exception: - pass return None