From c2f2fdb38b7d6f08c7edf06e5ff1efef81c091e6 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 2 Nov 2023 11:20:42 +0200 Subject: [PATCH 01/83] add workfile colorspace to Houdini settings --- .../defaults/project_settings/houdini.json | 6 ++++ .../schema_project_houdini.json | 34 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 87983620ec..93fa7e2c6f 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -21,6 +21,12 @@ "file_rules": { "activate_host_rules": false, "rules": {} + }, + "workfile":{ + "enabled": false, + "default_display": "ACES", + "default_view": "sRGB", + "review_color_space": "Output - sRGB" } }, "shelves": [], diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index d4d0565ec9..4e7f1aa4c9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -19,6 +19,40 @@ { "type": "template", "name": "template_host_color_management_ocio" + }, + { + "key": "workfile", + "type": "dict", + "label": "Workfile", + "collapsible": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "label", + "label": "Render space in Houdini is always set to 'scene_linear' Role." + }, + { + "type": "text", + "key": "default_display", + "label": "Display" + }, + { + "type": "text", + "key": "default_view", + "label": "View" + }, + { + "type": "text", + "key": "review_color_space", + "label": "Review colorspace" + } + + ] } ] }, From bf32236e9477205c3cb65ffaa60e5f0d5d21c357 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 2 Nov 2023 13:24:40 +0200 Subject: [PATCH 02/83] add Houdini workfile colorspace to Ayon settings --- .../houdini/server/settings/imageio.py | 34 ++++++++++++++++++- server_addon/houdini/server/settings/main.py | 6 +++- server_addon/houdini/server/version.py | 2 +- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/server_addon/houdini/server/settings/imageio.py b/server_addon/houdini/server/settings/imageio.py index 88aa40ecd6..6a61171b66 100644 --- a/server_addon/houdini/server/settings/imageio.py +++ b/server_addon/houdini/server/settings/imageio.py @@ -34,9 +34,18 @@ class ImageIOFileRulesModel(BaseSettingsModel): return value +class WorkfileImageIOModel(BaseSettingsModel): + """Render space in Houdini is always set to 'scene_linear' Role.""" + + enabled: bool = Field(False, title="Enabled") + default_display: str = Field(title="Display") + default_view: str = Field(title="View") + review_color_space: str = Field(title="Review colorspace") + + class HoudiniImageIOModel(BaseSettingsModel): activate_host_color_management: bool = Field( - True, title="Enable Color Management" + False, title="Enable Color Management" ) ocio_config: ImageIOConfigModel = Field( default_factory=ImageIOConfigModel, @@ -46,3 +55,26 @@ class HoudiniImageIOModel(BaseSettingsModel): default_factory=ImageIOFileRulesModel, title="File Rules" ) + workfile: WorkfileImageIOModel = Field( + default_factory=WorkfileImageIOModel, + title="Workfile" + ) + + +DEFAULT_IMAGEIO_SETTINGS = { + "activate_host_color_management": False, + "ocio_config": { + "override_global_config": False, + "filepath": [] + }, + "file_rules": { + "activate_host_rules": False, + "rules": [] + }, + "workfile": { + "enabled": False, + "default_display": "ACES", + "default_view": "sRGB", + "review_color_space": "Output - sRGB" + } +} diff --git a/server_addon/houdini/server/settings/main.py b/server_addon/houdini/server/settings/main.py index 9cfec54f22..250e654afd 100644 --- a/server_addon/houdini/server/settings/main.py +++ b/server_addon/houdini/server/settings/main.py @@ -4,7 +4,10 @@ from .general import ( GeneralSettingsModel, DEFAULT_GENERAL_SETTINGS ) -from .imageio import HoudiniImageIOModel +from .imageio import ( + HoudiniImageIOModel, + DEFAULT_IMAGEIO_SETTINGS +) from .shelves import ShelvesModel from .create import ( CreatePluginsModel, @@ -41,6 +44,7 @@ class HoudiniSettings(BaseSettingsModel): DEFAULT_VALUES = { "general": DEFAULT_GENERAL_SETTINGS, + "imageio": DEFAULT_IMAGEIO_SETTINGS, "shelves": [], "create": DEFAULT_HOUDINI_CREATE_SETTINGS, "publish": DEFAULT_HOUDINI_PUBLISH_SETTINGS diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 01ef12070d..6cd38b7465 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.2.6" +__version__ = "0.2.7" From fca6abde553beb286268299803c17f4d22e63d3f Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 2 Nov 2023 13:26:11 +0200 Subject: [PATCH 03/83] add Houdini SetDefaultDislayView prelaunch hook --- .../hooks/set_default_display_and_view.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 openpype/hosts/houdini/hooks/set_default_display_and_view.py diff --git a/openpype/hosts/houdini/hooks/set_default_display_and_view.py b/openpype/hosts/houdini/hooks/set_default_display_and_view.py new file mode 100644 index 0000000000..33278d7fee --- /dev/null +++ b/openpype/hosts/houdini/hooks/set_default_display_and_view.py @@ -0,0 +1,52 @@ +from openpype.lib.applications import PreLaunchHook, LaunchTypes +from openpype.settings import get_project_settings + + +class SetDefaultDislayView(PreLaunchHook): + """Set default view and default display for houdini hosts that use OpenColorIO. + + Houdini's defaultDisplay and defaultView are set by + setting 'OCIO_ACTIVE_DISPLAYS' and 'OCIO_ACTIVE_VIEWS' + environment variables respectively. + + More info: https://www.sidefx.com/docs/houdini/io/ocio.html#set-up + """ + + app_groups = {"houdini"} + launch_types = {LaunchTypes.local} + + def execute(self): + + OCIO = self.launch_context.env.get("OCIO") + + # This is a cheap way to skip this hook if either + # global color management or houdini color management was disabled. + if not OCIO: + return + + project_settings = get_project_settings( + project_name=self.data["project_name"] + ) + + houdini_color_Settings = project_settings["houdini"]["imageio"]["workfile"] + + if not houdini_color_Settings["enabled"] : + self.log.info( + "Houdini's workefile color settings are disabled." + ) + return + + default_display = houdini_color_Settings["default_display"] + default_view = houdini_color_Settings["default_view"] + + self.log.info( + "Setting OCIO_ACTIVE_DISPLAYS environment to : {}" + .format(default_display) + ) + self.launch_context.env["OCIO_ACTIVE_DISPLAYS"] = default_display + + self.log.info( + "Setting OCIO_ACTIVE_VIEWS environment to config path: {}" + .format(default_view) + ) + self.launch_context.env["OCIO_ACTIVE_VIEWS"] = default_view From 10cf562f359a39948b6db6245fd56533190d824d Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 2 Nov 2023 13:44:38 +0200 Subject: [PATCH 04/83] resolve hound --- .../hosts/houdini/hooks/set_default_display_and_view.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/hooks/set_default_display_and_view.py b/openpype/hosts/houdini/hooks/set_default_display_and_view.py index 33278d7fee..2c98247b8c 100644 --- a/openpype/hosts/houdini/hooks/set_default_display_and_view.py +++ b/openpype/hosts/houdini/hooks/set_default_display_and_view.py @@ -3,7 +3,7 @@ from openpype.settings import get_project_settings class SetDefaultDislayView(PreLaunchHook): - """Set default view and default display for houdini hosts that use OpenColorIO. + """Set default view and default display for houdini host that use OpenColorIO. Houdini's defaultDisplay and defaultView are set by setting 'OCIO_ACTIVE_DISPLAYS' and 'OCIO_ACTIVE_VIEWS' @@ -28,9 +28,10 @@ class SetDefaultDislayView(PreLaunchHook): project_name=self.data["project_name"] ) - houdini_color_Settings = project_settings["houdini"]["imageio"]["workfile"] + houdini_color_Settings = \ + project_settings["houdini"]["imageio"]["workfile"] - if not houdini_color_Settings["enabled"] : + if not houdini_color_Settings["enabled"]: self.log.info( "Houdini's workefile color settings are disabled." ) From 537d1acac854c46dbe20eac95977886f81619253 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 2 Nov 2023 15:38:11 +0200 Subject: [PATCH 05/83] BigRoy's comments - Better logging & remove unnecessary logic --- .../houdini/hooks/set_default_display_and_view.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/houdini/hooks/set_default_display_and_view.py b/openpype/hosts/houdini/hooks/set_default_display_and_view.py index 2c98247b8c..cb5dba51cf 100644 --- a/openpype/hosts/houdini/hooks/set_default_display_and_view.py +++ b/openpype/hosts/houdini/hooks/set_default_display_and_view.py @@ -1,5 +1,4 @@ from openpype.lib.applications import PreLaunchHook, LaunchTypes -from openpype.settings import get_project_settings class SetDefaultDislayView(PreLaunchHook): @@ -19,21 +18,18 @@ class SetDefaultDislayView(PreLaunchHook): OCIO = self.launch_context.env.get("OCIO") - # This is a cheap way to skip this hook if either - # global color management or houdini color management was disabled. + # This is a cheap way to skip this hook if either global color + # management or houdini color management was disabled because the + # OCIO var would be set by the global OCIOEnvHook if not OCIO: return - project_settings = get_project_settings( - project_name=self.data["project_name"] - ) - houdini_color_Settings = \ - project_settings["houdini"]["imageio"]["workfile"] + self.data["project_settings"]["houdini"]["imageio"]["workfile"] if not houdini_color_Settings["enabled"]: self.log.info( - "Houdini's workefile color settings are disabled." + "Houdini workfile color management is disabled." ) return From 3ffbf736517db28aef379ec1281c71753325e7f7 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 2 Nov 2023 15:45:22 +0200 Subject: [PATCH 06/83] resolve hound --- openpype/hosts/houdini/hooks/set_default_display_and_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/hooks/set_default_display_and_view.py b/openpype/hosts/houdini/hooks/set_default_display_and_view.py index cb5dba51cf..8c91ef7d06 100644 --- a/openpype/hosts/houdini/hooks/set_default_display_and_view.py +++ b/openpype/hosts/houdini/hooks/set_default_display_and_view.py @@ -25,7 +25,7 @@ class SetDefaultDislayView(PreLaunchHook): return houdini_color_Settings = \ - self.data["project_settings"]["houdini"]["imageio"]["workfile"] + self.data["project_settings"]["houdini"]["imageio"]["workfile"] if not houdini_color_Settings["enabled"]: self.log.info( From 1a1e48e4a5b44d1e0498952f78ad06a77f68e6f1 Mon Sep 17 00:00:00 2001 From: Mustafa Taher Date: Thu, 2 Nov 2023 16:23:27 +0200 Subject: [PATCH 07/83] BigRoy's comment - update variable name Co-authored-by: Roy Nieterau --- .../hosts/houdini/hooks/set_default_display_and_view.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/hooks/set_default_display_and_view.py b/openpype/hosts/houdini/hooks/set_default_display_and_view.py index 8c91ef7d06..85e605b37d 100644 --- a/openpype/hosts/houdini/hooks/set_default_display_and_view.py +++ b/openpype/hosts/houdini/hooks/set_default_display_and_view.py @@ -24,17 +24,17 @@ class SetDefaultDislayView(PreLaunchHook): if not OCIO: return - houdini_color_Settings = \ + houdini_color_settings = \ self.data["project_settings"]["houdini"]["imageio"]["workfile"] - if not houdini_color_Settings["enabled"]: + if not houdini_color_settings["enabled"]: self.log.info( "Houdini workfile color management is disabled." ) return - default_display = houdini_color_Settings["default_display"] - default_view = houdini_color_Settings["default_view"] + default_display = houdini_color_settings["default_display"] + default_view = houdini_color_settings["default_view"] self.log.info( "Setting OCIO_ACTIVE_DISPLAYS environment to : {}" From 7d80da84e21e615e32119c42e87b6e741356b7fa Mon Sep 17 00:00:00 2001 From: Mustafa Taher Date: Thu, 2 Nov 2023 17:18:18 +0200 Subject: [PATCH 08/83] BigRoy's Comment - fix typo Co-authored-by: Roy Nieterau --- openpype/hosts/houdini/hooks/set_default_display_and_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/hooks/set_default_display_and_view.py b/openpype/hosts/houdini/hooks/set_default_display_and_view.py index 85e605b37d..edff73117f 100644 --- a/openpype/hosts/houdini/hooks/set_default_display_and_view.py +++ b/openpype/hosts/houdini/hooks/set_default_display_and_view.py @@ -1,7 +1,7 @@ from openpype.lib.applications import PreLaunchHook, LaunchTypes -class SetDefaultDislayView(PreLaunchHook): +class SetDefaultDisplayView(PreLaunchHook): """Set default view and default display for houdini host that use OpenColorIO. Houdini's defaultDisplay and defaultView are set by From cd4d8bd2b1c9eddf4cbe7204999ead31d2308a4a Mon Sep 17 00:00:00 2001 From: Mustafa Taher Date: Fri, 3 Nov 2023 09:17:00 +0200 Subject: [PATCH 09/83] BigRoy's comment - better doc string Co-authored-by: Roy Nieterau --- openpype/hosts/houdini/hooks/set_default_display_and_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/hooks/set_default_display_and_view.py b/openpype/hosts/houdini/hooks/set_default_display_and_view.py index edff73117f..ff55e6275d 100644 --- a/openpype/hosts/houdini/hooks/set_default_display_and_view.py +++ b/openpype/hosts/houdini/hooks/set_default_display_and_view.py @@ -2,7 +2,7 @@ from openpype.lib.applications import PreLaunchHook, LaunchTypes class SetDefaultDisplayView(PreLaunchHook): - """Set default view and default display for houdini host that use OpenColorIO. + """Set default view and default display for houdini via OpenColorIO. Houdini's defaultDisplay and defaultView are set by setting 'OCIO_ACTIVE_DISPLAYS' and 'OCIO_ACTIVE_VIEWS' From 0a8e293e6e6157dd21efd439af874e095c488c5d Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 Nov 2023 09:25:07 +0200 Subject: [PATCH 10/83] bump patch version --- server_addon/houdini/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 6cd38b7465..c49a95c357 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.2.7" +__version__ = "0.2.8" From a32f0a62934a031ce25f9be24c83c2b7462699ed Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 6 Nov 2023 09:20:41 +0200 Subject: [PATCH 11/83] use 'review color space' setting in review creator --- .../houdini/plugins/create/create_review.py | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_review.py b/openpype/hosts/houdini/plugins/create/create_review.py index 60c34a358b..a7a56cce03 100644 --- a/openpype/hosts/houdini/plugins/create/create_review.py +++ b/openpype/hosts/houdini/plugins/create/create_review.py @@ -2,6 +2,7 @@ """Creator plugin for creating openGL reviews.""" from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef, BoolDef, NumberDef +from openpype.settings import get_current_project_settings import os import hou @@ -87,7 +88,7 @@ class CreateReview(plugin.HoudiniCreator): # Set OCIO Colorspace to the default output colorspace # if there's OCIO if os.getenv("OCIO"): - self.set_colorcorrect_to_default_view_space(instance_node) + self.set_review_color_space(instance_node) to_lock = ["id", "family"] @@ -131,22 +132,40 @@ class CreateReview(plugin.HoudiniCreator): decimals=3) ] - def set_colorcorrect_to_default_view_space(self, - instance_node): - """Set ociocolorspace to the default output space.""" - from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa + def set_review_color_space(self, instance_node): + """Set ociocolorspace parameter. - # set Color Correction parameter to OpenColorIO + This function will use the value exposed in settings + if workfile settings were enabled. + + Otherwise, it will use the default colorspace corresponding + to the display & view of the current Houdini session. + """ + + # Set Color Correction parameter to OpenColorIO instance_node.setParms({"colorcorrect": 2}) - # Get default view space for ociocolorspace parm. - default_view_space = get_default_display_view_colorspace() + # Get view space for ociocolorspace parm. + view_space = self.get_review_colorspace_from_Settings() + + if not view_space: + from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa + view_space = get_default_display_view_colorspace() + instance_node.setParms( - {"ociocolorspace": default_view_space} + {"ociocolorspace": view_space} ) self.log.debug( "'OCIO Colorspace' parm on '{}' has been set to " - "the default view color space '{}'" - .format(instance_node, default_view_space) + "the view color space '{}'" + .format(instance_node, view_space) ) + + def get_review_colorspace_from_Settings(self): + project_settings = get_current_project_settings() + color_settings = project_settings["houdini"]["imageio"]["workfile"] + if color_settings["enabled"]: + return color_settings.get("review_color_space") + + return "" From 0eacab216675438d65c697428916d055d57631de Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 6 Nov 2023 11:51:01 +0200 Subject: [PATCH 12/83] update houdini workfile color settings --- .../settings/defaults/project_settings/houdini.json | 3 ++- .../projects_schema/schema_project_houdini.json | 10 +++++++++- server_addon/houdini/server/settings/imageio.py | 10 +++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 93fa7e2c6f..826a4e12f1 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -26,7 +26,8 @@ "enabled": false, "default_display": "ACES", "default_view": "sRGB", - "review_color_space": "Output - sRGB" + "review_color_space": "Output - sRGB", + "override_review_color": false } }, "shelves": [], diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index 4e7f1aa4c9..70b6d63ee8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -50,8 +50,16 @@ "type": "text", "key": "review_color_space", "label": "Review colorspace" + }, + { + "type": "label", + "label": "Allow artists to override review colorspace, therefore review color space validator\nwon't error if artists used another review colorspace." + }, + { + "type": "boolean", + "key": "override_review_color", + "label": "Override review colorspace" } - ] } ] diff --git a/server_addon/houdini/server/settings/imageio.py b/server_addon/houdini/server/settings/imageio.py index 6a61171b66..6404942d5b 100644 --- a/server_addon/houdini/server/settings/imageio.py +++ b/server_addon/houdini/server/settings/imageio.py @@ -41,6 +41,13 @@ class WorkfileImageIOModel(BaseSettingsModel): default_display: str = Field(title="Display") default_view: str = Field(title="View") review_color_space: str = Field(title="Review colorspace") + override_review_color: bool = Field( + False, + title="Override review colorspace", + description=("Allow artists to override review colorspace, " + "therefore review color space validator won't error " + "if artists used another review colorspace.") + ) class HoudiniImageIOModel(BaseSettingsModel): @@ -76,5 +83,6 @@ DEFAULT_IMAGEIO_SETTINGS = { "default_display": "ACES", "default_view": "sRGB", "review_color_space": "Output - sRGB" - } + }, + "override_review_color": False } From c4cd10c63740243e97bf270d1b9de7c26f82ae98 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 6 Nov 2023 11:51:51 +0200 Subject: [PATCH 13/83] use houdini color settings while review creation and validation --- openpype/hosts/houdini/api/lib.py | 62 +++++++++++++++++++ .../houdini/plugins/create/create_review.py | 43 +------------ .../publish/validate_review_colorspace.py | 54 +++++++++------- 3 files changed, 94 insertions(+), 65 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index ac375c56d6..f4db492978 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -844,6 +844,68 @@ def get_current_context_template_data_with_asset_data(): return template_data +def get_houdini_color_settings(): + """Get Houdini working file color settings. + + Returns: + Dict: The dictionary contains the Houdini working file color settings + if the settings are enabled, otherwise it is an empty dictionary. + """ + + project_settings = get_current_project_settings() + color_settings = project_settings["houdini"]["imageio"]["workfile"] + if color_settings["enabled"]: + color_settings.pop("enabled") + # Remove leading, and trailing whitespaces + view_Space = color_settings["review_color_space"] + color_settings["review_color_space"] = view_Space.strip() + return color_settings + + return {} + +def set_review_color_space(opengl_node, log=None): + """Set ociocolorspace parameter for the given OpenGL node. + + This function will use the value exposed in settings + if workfile settings were enabled. + + Otherwise, it will use the default colorspace corresponding + to the display & view of the current Houdini session. + + Args: + OpenGl node (hou.Node): ROP node to set its ociocolorspace parameter. + log (logging.Logger): Logger to log to. + """ + + if log is None: + log = self.log + + # Set Color Correction parameter to OpenColorIO + if opengl_node.evalParm("colorcorrect") != 2: + opengl_node.setParms({"colorcorrect": 2}) + log.debug( + "'Color Correction' parm on '{}' has been set to" + " 'OpenColorIO'".format(opengl_node.path()) + ) + + # Get view space for ociocolorspace parm. + view_space = get_houdini_color_settings().get("review_color_space") + + if not view_space: + from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa + view_space = get_default_display_view_colorspace() + + opengl_node.setParms( + {"ociocolorspace": view_space} + ) + + self.log.debug( + "'OCIO Colorspace' parm on '{}' has been set to " + "the view color space '{}'" + .format(opengl_node, view_space) + ) + + def get_context_var_changes(): """get context var changes.""" diff --git a/openpype/hosts/houdini/plugins/create/create_review.py b/openpype/hosts/houdini/plugins/create/create_review.py index a7a56cce03..775babbab6 100644 --- a/openpype/hosts/houdini/plugins/create/create_review.py +++ b/openpype/hosts/houdini/plugins/create/create_review.py @@ -2,8 +2,7 @@ """Creator plugin for creating openGL reviews.""" from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef, BoolDef, NumberDef -from openpype.settings import get_current_project_settings - +from openpype.hosts.houdini.api.lib import set_review_color_space import os import hou @@ -88,7 +87,7 @@ class CreateReview(plugin.HoudiniCreator): # Set OCIO Colorspace to the default output colorspace # if there's OCIO if os.getenv("OCIO"): - self.set_review_color_space(instance_node) + set_review_color_space(instance_node, log=self.log) to_lock = ["id", "family"] @@ -131,41 +130,3 @@ class CreateReview(plugin.HoudiniCreator): minimum=0.0001, decimals=3) ] - - def set_review_color_space(self, instance_node): - """Set ociocolorspace parameter. - - This function will use the value exposed in settings - if workfile settings were enabled. - - Otherwise, it will use the default colorspace corresponding - to the display & view of the current Houdini session. - """ - - # Set Color Correction parameter to OpenColorIO - instance_node.setParms({"colorcorrect": 2}) - - # Get view space for ociocolorspace parm. - view_space = self.get_review_colorspace_from_Settings() - - if not view_space: - from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa - view_space = get_default_display_view_colorspace() - - instance_node.setParms( - {"ociocolorspace": view_space} - ) - - self.log.debug( - "'OCIO Colorspace' parm on '{}' has been set to " - "the view color space '{}'" - .format(instance_node, view_space) - ) - - def get_review_colorspace_from_Settings(self): - project_settings = get_current_project_settings() - color_settings = project_settings["houdini"]["imageio"]["workfile"] - if color_settings["enabled"]: - return color_settings.get("review_color_space") - - return "" diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 03ecd1b052..2bc62516d5 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -6,13 +6,17 @@ from openpype.pipeline import ( ) from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api.action import SelectROPAction +from openpype.hosts.houdini.api.lib import ( + get_houdini_color_settings, + set_review_color_space +) import os import hou -class SetDefaultViewSpaceAction(RepairAction): - label = "Set default view colorspace" +class SetReviewColorSpaceAction(RepairAction): + label = "Set Review Color Space" icon = "mdi.monitor" @@ -27,7 +31,7 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, families = ["review"] hosts = ["houdini"] label = "Validate Review Colorspace" - actions = [SetDefaultViewSpaceAction, SelectROPAction] + actions = [SetReviewColorSpaceAction, SelectROPAction] optional = True @@ -61,30 +65,32 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, .format(rop_node.path()) ) + color_settings = get_houdini_color_settings() + if color_settings.get("override_review_color"): + return + + if rop_node.evalParm("ociocolorspace") != \ + color_settings["review_color_space"]: + + raise PublishValidationError( + "Invalid value: Colorspace name doesn't match studio settings.\n" + "Check 'OCIO Colorspace' parameter on '{}' ROP" + .format(rop_node.path()) + ) + @classmethod def repair(cls, instance): """Set Default View Space Action. - It is a helper action more than a repair action, - used to set colorspace on opengl node to the default view. + It sets ociocolorspace parameter. + + if workfile settings are enabled, it will use the value + exposed in the settings. + + if workfile settings are disabled, it will use the default + colorspace corresponding to the display & view of + the current Houdini session. """ - from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa - rop_node = hou.node(instance.data["instance_node"]) - - if rop_node.evalParm("colorcorrect") != 2: - rop_node.setParms({"colorcorrect": 2}) - cls.log.debug( - "'Color Correction' parm on '{}' has been set to" - " 'OpenColorIO'".format(rop_node.path()) - ) - - # Get default view colorspace name - default_view_space = get_default_display_view_colorspace() - - rop_node.setParms({"ociocolorspace": default_view_space}) - cls.log.info( - "'OCIO Colorspace' parm on '{}' has been set to " - "the default view color space '{}'" - .format(rop_node, default_view_space) - ) + opengl_node = hou.node(instance.data["instance_node"]) + set_review_color_space(opengl_node, log=cls.log) From 2caac26ac7c3c0dc8d0262488840162d5bd5e09e Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 6 Nov 2023 11:55:46 +0200 Subject: [PATCH 14/83] Resolve Hound --- openpype/hosts/houdini/api/lib.py | 1 + .../houdini/plugins/publish/validate_review_colorspace.py | 4 ++-- server_addon/houdini/server/settings/imageio.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index ae65cadf1b..bfbfcb586e 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -855,6 +855,7 @@ def get_houdini_color_settings(): return {} + def set_review_color_space(opengl_node, log=None): """Set ociocolorspace parameter for the given OpenGL node. diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 2bc62516d5..783e67014c 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -73,8 +73,8 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, color_settings["review_color_space"]: raise PublishValidationError( - "Invalid value: Colorspace name doesn't match studio settings.\n" - "Check 'OCIO Colorspace' parameter on '{}' ROP" + "Invalid value: Colorspace name doesn't match studio " + "settings.\nCheck 'OCIO Colorspace' parameter on '{}' ROP" .format(rop_node.path()) ) diff --git a/server_addon/houdini/server/settings/imageio.py b/server_addon/houdini/server/settings/imageio.py index 6404942d5b..ab98152232 100644 --- a/server_addon/houdini/server/settings/imageio.py +++ b/server_addon/houdini/server/settings/imageio.py @@ -45,8 +45,8 @@ class WorkfileImageIOModel(BaseSettingsModel): False, title="Override review colorspace", description=("Allow artists to override review colorspace, " - "therefore review color space validator won't error " - "if artists used another review colorspace.") + "therefore review color space validator won't error " + "if artists used another review colorspace.") ) From 8de9dccbfb4734452286fc6dc55d0d9cbcfef4ed Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 6 Nov 2023 12:07:10 +0200 Subject: [PATCH 15/83] update doc string --- openpype/hosts/houdini/api/lib.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index bfbfcb586e..b40f8b2fcf 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -859,11 +859,12 @@ def get_houdini_color_settings(): def set_review_color_space(opengl_node, log=None): """Set ociocolorspace parameter for the given OpenGL node. - This function will use the value exposed in settings - if workfile settings were enabled. + if workfile settings are enabled, it will use the value + exposed in the settings. - Otherwise, it will use the default colorspace corresponding - to the display & view of the current Houdini session. + if workfile settings are disabled, it will use the default + colorspace corresponding to the display & view of + the current Houdini session. Args: OpenGl node (hou.Node): ROP node to set its ociocolorspace parameter. From 96849785936881a79ea1a325a643a957a6612f4d Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 6 Nov 2023 12:27:17 +0200 Subject: [PATCH 16/83] bump Houdini addon patch version --- server_addon/houdini/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index c49a95c357..75cf7831c4 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.2.8" +__version__ = "0.2.9" From dadddfb00e9e8c9ee52ffc19960c2db4333a65ab Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 7 Nov 2023 08:18:10 +0200 Subject: [PATCH 17/83] Minkiu comment - remove unnecessary variable --- openpype/hosts/houdini/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index b40f8b2fcf..27e70ce152 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -849,8 +849,8 @@ def get_houdini_color_settings(): if color_settings["enabled"]: color_settings.pop("enabled") # Remove leading, and trailing whitespaces - view_Space = color_settings["review_color_space"] - color_settings["review_color_space"] = view_Space.strip() + color_settings["review_color_space"] = \ + color_settings["review_color_space"].strip() return color_settings return {} From a1d1c49e3866c4cc5f9fad235f2822e308423daa Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 7 Nov 2023 08:36:01 +0200 Subject: [PATCH 18/83] fabia's comment - Better conditional --- openpype/hosts/houdini/api/lib.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 27e70ce152..bdcc368d7f 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -875,8 +875,9 @@ def set_review_color_space(opengl_node, log=None): log = self.log # Set Color Correction parameter to OpenColorIO - if opengl_node.evalParm("colorcorrect") != 2: - opengl_node.setParms({"colorcorrect": 2}) + colorcorrect_parm = opengl_node.parm("colorcorrect") + if colorcorrect_parm.eval() != 2: + colorcorrect_parm.set(2) log.debug( "'Color Correction' parm on '{}' has been set to" " 'OpenColorIO'".format(opengl_node.path()) From 83089da81a93c62c65cf8a768f9891e63637898a Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 7 Nov 2023 10:43:53 +0200 Subject: [PATCH 19/83] update set_review_color_space() and get_houdini_color_settings() logic --- openpype/hosts/houdini/api/lib.py | 23 +++++++-------- .../publish/validate_review_colorspace.py | 29 ++++++++++--------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index bdcc368d7f..77ae929857 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -846,25 +846,22 @@ def get_houdini_color_settings(): project_settings = get_current_project_settings() color_settings = project_settings["houdini"]["imageio"]["workfile"] - if color_settings["enabled"]: - color_settings.pop("enabled") - # Remove leading, and trailing whitespaces - color_settings["review_color_space"] = \ - color_settings["review_color_space"].strip() - return color_settings - return {} + # Remove leading, and trailing whitespaces + color_settings["review_color_space"] = \ + color_settings["review_color_space"].strip() + return color_settings def set_review_color_space(opengl_node, log=None): """Set ociocolorspace parameter for the given OpenGL node. - if workfile settings are enabled, it will use the value + If workfile settings are enabled, it will use the value exposed in the settings. - if workfile settings are disabled, it will use the default - colorspace corresponding to the display & view of - the current Houdini session. + If the value exposed in the settings is empty, + it will use the default colorspace corresponding to + the display & view of the current Houdini session. Args: OpenGl node (hou.Node): ROP node to set its ociocolorspace parameter. @@ -884,8 +881,10 @@ def set_review_color_space(opengl_node, log=None): ) # Get view space for ociocolorspace parm. - view_space = get_houdini_color_settings().get("review_color_space") + color_settings = get_houdini_color_settings() + view_space = color_settings["review_color_space"] if color_settings["enabled"] else "" # noqa + # fall to default review color space if the setting is empty. if not view_space: from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa view_space = get_default_display_view_colorspace() diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 783e67014c..e4356a741c 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -66,17 +66,18 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, ) color_settings = get_houdini_color_settings() - if color_settings.get("override_review_color"): - return + # skip if houdini color settings are disabled + if color_settings["enabled"]: + view_space = color_settings["review_color_space"] + # skip if review color space setting is empty. + if view_space and \ + rop_node.evalParm("ociocolorspace") != view_space: - if rop_node.evalParm("ociocolorspace") != \ - color_settings["review_color_space"]: - - raise PublishValidationError( - "Invalid value: Colorspace name doesn't match studio " - "settings.\nCheck 'OCIO Colorspace' parameter on '{}' ROP" - .format(rop_node.path()) - ) + raise PublishValidationError( + "Invalid value: Colorspace name doesn't match studio " + "settings.\nCheck 'OCIO Colorspace' parameter on '{}' ROP" + .format(rop_node.path()) + ) @classmethod def repair(cls, instance): @@ -84,12 +85,12 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, It sets ociocolorspace parameter. - if workfile settings are enabled, it will use the value + If workfile settings are enabled, it will use the value exposed in the settings. - if workfile settings are disabled, it will use the default - colorspace corresponding to the display & view of - the current Houdini session. + If the value exposed in the settings is empty, + it will use the default colorspace corresponding to + the display & view of the current Houdini session. """ opengl_node = hou.node(instance.data["instance_node"]) From f349df03407e67ac91e19292b250c00f58c9ddb7 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 7 Nov 2023 10:52:50 +0200 Subject: [PATCH 20/83] undo adding unnecessary setting --- .../defaults/project_settings/houdini.json | 3 +-- .../projects_schema/schema_project_houdini.json | 7 +------ server_addon/houdini/server/settings/imageio.py | 16 ++++++---------- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 017682782d..7473e83275 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -26,8 +26,7 @@ "enabled": false, "default_display": "ACES", "default_view": "sRGB", - "review_color_space": "Output - sRGB", - "override_review_color": false + "review_color_space": "" } }, "shelves": [], diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index 70b6d63ee8..af87f4ff35 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -53,12 +53,7 @@ }, { "type": "label", - "label": "Allow artists to override review colorspace, therefore review color space validator\nwon't error if artists used another review colorspace." - }, - { - "type": "boolean", - "key": "override_review_color", - "label": "Override review colorspace" + "label": "Leave Review colorspace empty to use the default colorspace." } ] } diff --git a/server_addon/houdini/server/settings/imageio.py b/server_addon/houdini/server/settings/imageio.py index ab98152232..9e9c2a6092 100644 --- a/server_addon/houdini/server/settings/imageio.py +++ b/server_addon/houdini/server/settings/imageio.py @@ -40,13 +40,10 @@ class WorkfileImageIOModel(BaseSettingsModel): enabled: bool = Field(False, title="Enabled") default_display: str = Field(title="Display") default_view: str = Field(title="View") - review_color_space: str = Field(title="Review colorspace") - override_review_color: bool = Field( - False, - title="Override review colorspace", - description=("Allow artists to override review colorspace, " - "therefore review color space validator won't error " - "if artists used another review colorspace.") + review_color_space: str = Field( + title="Review colorspace", + description=("Leave Review colorspace empty to use the" + " default colorspace.") ) @@ -82,7 +79,6 @@ DEFAULT_IMAGEIO_SETTINGS = { "enabled": False, "default_display": "ACES", "default_view": "sRGB", - "review_color_space": "Output - sRGB" - }, - "override_review_color": False + "review_color_space": "" + } } From fd648509626edafdc4f545e5b4ac7c9b50482d73 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 8 Nov 2023 17:43:38 +0200 Subject: [PATCH 21/83] add colorspace data to houdini review extractor --- openpype/hosts/houdini/plugins/publish/extract_opengl.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/extract_opengl.py b/openpype/hosts/houdini/plugins/publish/extract_opengl.py index 38808089ac..25203f444e 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_opengl.py +++ b/openpype/hosts/houdini/plugins/publish/extract_opengl.py @@ -8,7 +8,8 @@ from openpype.hosts.houdini.api.lib import render_rop import hou -class ExtractOpenGL(publish.Extractor): +class ExtractOpenGL(publish.Extractor, + publish.ColormanagedPyblishPluginMixin): order = pyblish.api.ExtractorOrder - 0.01 label = "Extract OpenGL" @@ -46,6 +47,12 @@ class ExtractOpenGL(publish.Extractor): "camera_name": instance.data.get("review_camera") } + colorspace = ropnode.evalParm("ociocolorspace") + # inject colorspace data + self.set_representation_colorspace( + representation, instance.context, + colorspace=colorspace + ) if "representations" not in instance.data: instance.data["representations"] = [] instance.data["representations"].append(representation) From 205c0642d52ee9cf4f035b3418be75eaabc4848e Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 8 Nov 2023 18:00:02 +0200 Subject: [PATCH 22/83] BigRoy's comment - use a conditional --- .../houdini/plugins/publish/extract_opengl.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/extract_opengl.py b/openpype/hosts/houdini/plugins/publish/extract_opengl.py index 25203f444e..e6579e03a3 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_opengl.py +++ b/openpype/hosts/houdini/plugins/publish/extract_opengl.py @@ -47,12 +47,14 @@ class ExtractOpenGL(publish.Extractor, "camera_name": instance.data.get("review_camera") } - colorspace = ropnode.evalParm("ociocolorspace") - # inject colorspace data - self.set_representation_colorspace( - representation, instance.context, - colorspace=colorspace - ) + if ropnode.evalParm("colorcorrect") == 2: # OpenColorIO enabled + colorspace = ropnode.evalParm("ociocolorspace") + # inject colorspace data + self.set_representation_colorspace( + representation, instance.context, + colorspace=colorspace + ) + if "representations" not in instance.data: instance.data["representations"] = [] instance.data["representations"].append(representation) From 7a62d5d9b866b162a2f9d6f18ea947d171147ca1 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 9 Nov 2023 17:18:27 +0200 Subject: [PATCH 23/83] add colorspace data to houdini image sequence extractor --- .../houdini/plugins/publish/extract_composite.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/extract_composite.py b/openpype/hosts/houdini/plugins/publish/extract_composite.py index 11cf83a46d..5047b719c9 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_composite.py +++ b/openpype/hosts/houdini/plugins/publish/extract_composite.py @@ -7,7 +7,8 @@ from openpype.hosts.houdini.api.lib import render_rop, splitext import hou -class ExtractComposite(publish.Extractor): +class ExtractComposite(publish.Extractor, + publish.ColormanagedPyblishPluginMixin): order = pyblish.api.ExtractorOrder label = "Extract Composite (Image Sequence)" @@ -45,8 +46,11 @@ class ExtractComposite(publish.Extractor): "frameEnd": instance.data["frameEndHandle"], } - from pprint import pformat - - self.log.info(pformat(representation)) + # inject colorspace data + # It's always scene_linear (Houdini's default) + self.set_representation_colorspace( + representation, instance.context, + colorspace="scene_linear" + ) instance.data["representations"].append(representation) From 251291aa38e0cccc560ae9d460f4b0f4cc7453f4 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Sun, 12 Nov 2023 11:37:38 +0200 Subject: [PATCH 24/83] append existent view and display env vars --- .../houdini/hooks/set_default_display_and_view.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/hooks/set_default_display_and_view.py b/openpype/hosts/houdini/hooks/set_default_display_and_view.py index ff55e6275d..54a1e39a87 100644 --- a/openpype/hosts/houdini/hooks/set_default_display_and_view.py +++ b/openpype/hosts/houdini/hooks/set_default_display_and_view.py @@ -1,5 +1,5 @@ from openpype.lib.applications import PreLaunchHook, LaunchTypes - +import os class SetDefaultDisplayView(PreLaunchHook): """Set default view and default display for houdini via OpenColorIO. @@ -33,8 +33,18 @@ class SetDefaultDisplayView(PreLaunchHook): ) return + # This is a way to get values specified by admins if they already + # added 'OCIO_ACTIVE_DISPLAYS', 'OCIO_ACTIVE_VIEWS' manually + # using Ayon global env vars or Ayon app env vars or Ayon houdini tool + OCIO_ACTIVE_DISPLAYS = self.launch_context.env.get("OCIO_ACTIVE_DISPLAYS", "") + OCIO_ACTIVE_VIEWS = self.launch_context.env.get("OCIO_ACTIVE_VIEWS", "") + + # default_display and default_view default_display = houdini_color_settings["default_display"] + default_display = ":".join([default_display, OCIO_ACTIVE_DISPLAYS]) + default_view = houdini_color_settings["default_view"] + default_view = ":".join([default_view, OCIO_ACTIVE_VIEWS]) self.log.info( "Setting OCIO_ACTIVE_DISPLAYS environment to : {}" From 38bf9c17c4c2c992d92d3a4adf511e9bee5ec0f2 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Sun, 12 Nov 2023 11:40:05 +0200 Subject: [PATCH 25/83] resolve hound --- .../houdini/hooks/set_default_display_and_view.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/hooks/set_default_display_and_view.py b/openpype/hosts/houdini/hooks/set_default_display_and_view.py index 54a1e39a87..14f4fc5829 100644 --- a/openpype/hosts/houdini/hooks/set_default_display_and_view.py +++ b/openpype/hosts/houdini/hooks/set_default_display_and_view.py @@ -1,5 +1,5 @@ from openpype.lib.applications import PreLaunchHook, LaunchTypes -import os + class SetDefaultDisplayView(PreLaunchHook): """Set default view and default display for houdini via OpenColorIO. @@ -35,9 +35,14 @@ class SetDefaultDisplayView(PreLaunchHook): # This is a way to get values specified by admins if they already # added 'OCIO_ACTIVE_DISPLAYS', 'OCIO_ACTIVE_VIEWS' manually - # using Ayon global env vars or Ayon app env vars or Ayon houdini tool - OCIO_ACTIVE_DISPLAYS = self.launch_context.env.get("OCIO_ACTIVE_DISPLAYS", "") - OCIO_ACTIVE_VIEWS = self.launch_context.env.get("OCIO_ACTIVE_VIEWS", "") + # using Ayon global env vars or Ayon app env vars + # or Ayon houdini tool + OCIO_ACTIVE_DISPLAYS = self.launch_context.env.get( + "OCIO_ACTIVE_DISPLAYS", "" + ) + OCIO_ACTIVE_VIEWS = self.launch_context.env.get( + "OCIO_ACTIVE_VIEWS", "" + ) # default_display and default_view default_display = houdini_color_settings["default_display"] From 87dc8ca57984a3e1fc5ea2505ee227677d9fd4d6 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 13 Nov 2023 11:52:13 +0200 Subject: [PATCH 26/83] fix a typo --- openpype/hosts/houdini/hooks/set_default_display_and_view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/hooks/set_default_display_and_view.py b/openpype/hosts/houdini/hooks/set_default_display_and_view.py index 14f4fc5829..82d402a86b 100644 --- a/openpype/hosts/houdini/hooks/set_default_display_and_view.py +++ b/openpype/hosts/houdini/hooks/set_default_display_and_view.py @@ -52,13 +52,13 @@ class SetDefaultDisplayView(PreLaunchHook): default_view = ":".join([default_view, OCIO_ACTIVE_VIEWS]) self.log.info( - "Setting OCIO_ACTIVE_DISPLAYS environment to : {}" + "Setting OCIO_ACTIVE_DISPLAYS environment to: {}" .format(default_display) ) self.launch_context.env["OCIO_ACTIVE_DISPLAYS"] = default_display self.log.info( - "Setting OCIO_ACTIVE_VIEWS environment to config path: {}" + "Setting OCIO_ACTIVE_VIEWS environment to: {}" .format(default_view) ) self.launch_context.env["OCIO_ACTIVE_VIEWS"] = default_view From a7e2267eef6abeab2edbd876619deee377ff7e30 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 13 Nov 2023 14:28:51 +0200 Subject: [PATCH 27/83] BigRoy's comment - Cleaner env var values --- .../hooks/set_default_display_and_view.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/houdini/hooks/set_default_display_and_view.py b/openpype/hosts/houdini/hooks/set_default_display_and_view.py index 82d402a86b..243f32fcf6 100644 --- a/openpype/hosts/houdini/hooks/set_default_display_and_view.py +++ b/openpype/hosts/houdini/hooks/set_default_display_and_view.py @@ -46,19 +46,23 @@ class SetDefaultDisplayView(PreLaunchHook): # default_display and default_view default_display = houdini_color_settings["default_display"] - default_display = ":".join([default_display, OCIO_ACTIVE_DISPLAYS]) - - default_view = houdini_color_settings["default_view"] - default_view = ":".join([default_view, OCIO_ACTIVE_VIEWS]) - - self.log.info( + if default_display: + default_display = ":".join( + key for key in [default_display, OCIO_ACTIVE_DISPLAYS] if key + ) + self.log.info( "Setting OCIO_ACTIVE_DISPLAYS environment to: {}" .format(default_display) - ) - self.launch_context.env["OCIO_ACTIVE_DISPLAYS"] = default_display + ) + self.launch_context.env["OCIO_ACTIVE_DISPLAYS"] = default_display - self.log.info( - "Setting OCIO_ACTIVE_VIEWS environment to: {}" - .format(default_view) - ) - self.launch_context.env["OCIO_ACTIVE_VIEWS"] = default_view + default_view = houdini_color_settings["default_view"] + if default_view: + default_view = ":".join( + key for key in [default_view, OCIO_ACTIVE_VIEWS] if key + ) + self.log.info( + "Setting OCIO_ACTIVE_VIEWS environment to: {}" + .format(default_view) + ) + self.launch_context.env["OCIO_ACTIVE_VIEWS"] = default_view From a82367603f95ee554aefd673f9fae30018ee434d Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 13 Nov 2023 14:37:00 +0200 Subject: [PATCH 28/83] update houdini workfile colorsettings help --- .../schemas/projects_schema/schema_project_houdini.json | 4 ---- server_addon/houdini/server/settings/imageio.py | 6 +----- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index af87f4ff35..45ed6024db 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -50,10 +50,6 @@ "type": "text", "key": "review_color_space", "label": "Review colorspace" - }, - { - "type": "label", - "label": "Leave Review colorspace empty to use the default colorspace." } ] } diff --git a/server_addon/houdini/server/settings/imageio.py b/server_addon/houdini/server/settings/imageio.py index 9e9c2a6092..cc12a15c0d 100644 --- a/server_addon/houdini/server/settings/imageio.py +++ b/server_addon/houdini/server/settings/imageio.py @@ -40,11 +40,7 @@ class WorkfileImageIOModel(BaseSettingsModel): enabled: bool = Field(False, title="Enabled") default_display: str = Field(title="Display") default_view: str = Field(title="View") - review_color_space: str = Field( - title="Review colorspace", - description=("Leave Review colorspace empty to use the" - " default colorspace.") - ) + review_color_space: str = Field(title="Review colorspace") class HoudiniImageIOModel(BaseSettingsModel): From cb9e22ea6a33b319b8cd9a363fab3055cf2e3512 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 13 Nov 2023 14:38:29 +0200 Subject: [PATCH 29/83] resolve hound --- openpype/hosts/houdini/hooks/set_default_display_and_view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/hooks/set_default_display_and_view.py b/openpype/hosts/houdini/hooks/set_default_display_and_view.py index 243f32fcf6..0cfc23c930 100644 --- a/openpype/hosts/houdini/hooks/set_default_display_and_view.py +++ b/openpype/hosts/houdini/hooks/set_default_display_and_view.py @@ -51,8 +51,8 @@ class SetDefaultDisplayView(PreLaunchHook): key for key in [default_display, OCIO_ACTIVE_DISPLAYS] if key ) self.log.info( - "Setting OCIO_ACTIVE_DISPLAYS environment to: {}" - .format(default_display) + "Setting OCIO_ACTIVE_DISPLAYS environment to: {}" + .format(default_display) ) self.launch_context.env["OCIO_ACTIVE_DISPLAYS"] = default_display From ebc3d95773212d00076374f0808ab14173a741ce Mon Sep 17 00:00:00 2001 From: Mustafa Taher Date: Mon, 13 Nov 2023 15:16:01 +0200 Subject: [PATCH 30/83] BigRoy's suggestion - Better help text Co-authored-by: Roy Nieterau --- .../schemas/projects_schema/schema_project_houdini.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index 45ed6024db..275fc54053 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -34,7 +34,7 @@ }, { "type": "label", - "label": "Render space in Houdini is always set to 'scene_linear' Role." + "label": "Empty values will be skipped, allowing any existing env vars to pass through as defined.\nNote: The render space in Houdini is always set to the 'scene_linear' role." }, { "type": "text", From be6d015b45fa67710dd5d7acb41d252f2ed2d97f Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 13 Nov 2023 15:32:52 +0200 Subject: [PATCH 31/83] BigRoy's suggestion - Better help text, Ayon settings --- server_addon/houdini/server/settings/imageio.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server_addon/houdini/server/settings/imageio.py b/server_addon/houdini/server/settings/imageio.py index cc12a15c0d..4fa5cac82d 100644 --- a/server_addon/houdini/server/settings/imageio.py +++ b/server_addon/houdini/server/settings/imageio.py @@ -35,7 +35,13 @@ class ImageIOFileRulesModel(BaseSettingsModel): class WorkfileImageIOModel(BaseSettingsModel): - """Render space in Houdini is always set to 'scene_linear' Role.""" + """Workfile settings help. + + Empty values will be skipped, allowing any existing env vars to + pass through as defined. + + Note: The render space in Houdini is + always set to the 'scene_linear' role.""" enabled: bool = Field(False, title="Enabled") default_display: str = Field(title="Display") From d3362eb192a5f4b8c0eaf5fe54eca660a5a8d79a Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 13 Nov 2023 15:33:49 +0200 Subject: [PATCH 32/83] bump houdini addon version --- server_addon/houdini/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 75cf7831c4..6232f7ab18 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.2.9" +__version__ = "0.2.10" From 0c3ff0f4918884eb90b87f17f43c549fcdb56363 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 17 Nov 2023 21:38:10 +0200 Subject: [PATCH 33/83] Fabia's Comments --- openpype/hosts/houdini/api/lib.py | 12 +++++------ .../hooks/set_default_display_and_view.py | 21 +++++++++---------- .../plugins/publish/extract_composite.py | 15 +++++++------ .../publish/validate_review_colorspace.py | 10 ++++----- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 77ae929857..d506df429b 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -880,23 +880,23 @@ def set_review_color_space(opengl_node, log=None): " 'OpenColorIO'".format(opengl_node.path()) ) - # Get view space for ociocolorspace parm. + # Get review color space color_settings = get_houdini_color_settings() - view_space = color_settings["review_color_space"] if color_settings["enabled"] else "" # noqa + review_color_space = color_settings["review_color_space"] if color_settings["enabled"] else "" # noqa # fall to default review color space if the setting is empty. - if not view_space: + if not review_color_space: from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa - view_space = get_default_display_view_colorspace() + review_color_space = get_default_display_view_colorspace() opengl_node.setParms( - {"ociocolorspace": view_space} + {"ociocolorspace": review_color_space} ) self.log.debug( "'OCIO Colorspace' parm on '{}' has been set to " "the view color space '{}'" - .format(opengl_node, view_space) + .format(opengl_node, review_color_space) ) diff --git a/openpype/hosts/houdini/hooks/set_default_display_and_view.py b/openpype/hosts/houdini/hooks/set_default_display_and_view.py index 0cfc23c930..bb5f8bf71a 100644 --- a/openpype/hosts/houdini/hooks/set_default_display_and_view.py +++ b/openpype/hosts/houdini/hooks/set_default_display_and_view.py @@ -33,20 +33,15 @@ class SetDefaultDisplayView(PreLaunchHook): ) return - # This is a way to get values specified by admins if they already - # added 'OCIO_ACTIVE_DISPLAYS', 'OCIO_ACTIVE_VIEWS' manually - # using Ayon global env vars or Ayon app env vars - # or Ayon houdini tool - OCIO_ACTIVE_DISPLAYS = self.launch_context.env.get( - "OCIO_ACTIVE_DISPLAYS", "" - ) - OCIO_ACTIVE_VIEWS = self.launch_context.env.get( - "OCIO_ACTIVE_VIEWS", "" - ) + # 'OCIO_ACTIVE_DISPLAYS', 'OCIO_ACTIVE_VIEWS' are checked + # as Admins can add them in Ayon env vars or Ayon tools. - # default_display and default_view default_display = houdini_color_settings["default_display"] if default_display: + # get 'OCIO_ACTIVE_DISPLAYS' value if exists. + OCIO_ACTIVE_DISPLAYS = self.launch_context.env.get( + "OCIO_ACTIVE_DISPLAYS", "" + ) default_display = ":".join( key for key in [default_display, OCIO_ACTIVE_DISPLAYS] if key ) @@ -58,6 +53,10 @@ class SetDefaultDisplayView(PreLaunchHook): default_view = houdini_color_settings["default_view"] if default_view: + # get 'OCIO_ACTIVE_VIEWS' value if exists. + OCIO_ACTIVE_VIEWS = self.launch_context.env.get( + "OCIO_ACTIVE_VIEWS", "" + ) default_view = ":".join( key for key in [default_view, OCIO_ACTIVE_VIEWS] if key ) diff --git a/openpype/hosts/houdini/plugins/publish/extract_composite.py b/openpype/hosts/houdini/plugins/publish/extract_composite.py index 5047b719c9..a3194026bf 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_composite.py +++ b/openpype/hosts/houdini/plugins/publish/extract_composite.py @@ -46,11 +46,14 @@ class ExtractComposite(publish.Extractor, "frameEnd": instance.data["frameEndHandle"], } - # inject colorspace data - # It's always scene_linear (Houdini's default) - self.set_representation_colorspace( - representation, instance.context, - colorspace="scene_linear" - ) + if ext == "exr": + # Inject colorspace with 'scene_linear' as that's the + # default Houdini working colorspace and all extracted + # OpenEXR images should be in that colorspace. + # https://www.sidefx.com/docs/houdini/render/linear.html#image-formats + self.set_representation_colorspace( + representation, instance.context, + colorspace="scene_linear" + ) instance.data["representations"].append(representation) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index e4356a741c..db6533d4ec 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -16,7 +16,7 @@ import hou class SetReviewColorSpaceAction(RepairAction): - label = "Set Review Color Space" + label = "Set Default Review Color Space" icon = "mdi.monitor" @@ -68,10 +68,10 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, color_settings = get_houdini_color_settings() # skip if houdini color settings are disabled if color_settings["enabled"]: - view_space = color_settings["review_color_space"] + review_color_space = color_settings["review_color_space"] # skip if review color space setting is empty. - if view_space and \ - rop_node.evalParm("ociocolorspace") != view_space: + if review_color_space and \ + rop_node.evalParm("ociocolorspace") != review_color_space: raise PublishValidationError( "Invalid value: Colorspace name doesn't match studio " @@ -81,7 +81,7 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, @classmethod def repair(cls, instance): - """Set Default View Space Action. + """Set Default Review Space Action. It sets ociocolorspace parameter. From e45df04e5c63a998167863832d73838d879bb98e Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 23 Nov 2023 09:24:15 +0200 Subject: [PATCH 34/83] Jeza Comment - Better Labels --- .../schemas/projects_schema/schema_project_houdini.json | 4 ++-- server_addon/houdini/server/settings/imageio.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index 275fc54053..9aef7c3525 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -39,12 +39,12 @@ { "type": "text", "key": "default_display", - "label": "Display" + "label": "Default active displays" }, { "type": "text", "key": "default_view", - "label": "View" + "label": "Default active views" }, { "type": "text", diff --git a/server_addon/houdini/server/settings/imageio.py b/server_addon/houdini/server/settings/imageio.py index 4fa5cac82d..694d653720 100644 --- a/server_addon/houdini/server/settings/imageio.py +++ b/server_addon/houdini/server/settings/imageio.py @@ -44,8 +44,8 @@ class WorkfileImageIOModel(BaseSettingsModel): always set to the 'scene_linear' role.""" enabled: bool = Field(False, title="Enabled") - default_display: str = Field(title="Display") - default_view: str = Field(title="View") + default_display: str = Field(title="Default active displays") + default_view: str = Field(title="Default active views") review_color_space: str = Field(title="Review colorspace") From ab04429f5d6424ceacfc23705ec5008b157fbe53 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 24 Nov 2023 16:23:09 +0200 Subject: [PATCH 35/83] BigRoy's comment - add settings tip --- .../houdini/server/settings/imageio.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/server_addon/houdini/server/settings/imageio.py b/server_addon/houdini/server/settings/imageio.py index 694d653720..18ebc2c0a1 100644 --- a/server_addon/houdini/server/settings/imageio.py +++ b/server_addon/houdini/server/settings/imageio.py @@ -44,9 +44,22 @@ class WorkfileImageIOModel(BaseSettingsModel): always set to the 'scene_linear' role.""" enabled: bool = Field(False, title="Enabled") - default_display: str = Field(title="Default active displays") - default_view: str = Field(title="Default active views") - review_color_space: str = Field(title="Review colorspace") + default_display: str = Field( + title="Default active displays", + description="It behaves like the 'OCIO_ACTIVE_DISPLAYS' env var," + " Colon-separated list of displays, e.g ACES:P3" + ) + default_view: str = Field( + title="Default active views", + description="It behaves like the 'OCIO_ACTIVE_VIEWS' env var," + " Colon-separated list of views, e.g sRGB:DCDM" + ) + review_color_space: str = Field( + title="Review colorspace", + description="It exposes OCIO Colorspace parameter in opengl nodes." + "if left empty, Ayon will figure out the default " + "colorspace using your default display and default view." + ) class HoudiniImageIOModel(BaseSettingsModel): From c40b7139d07861ee869f67d209810d867a37d0e6 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 14 Dec 2023 14:49:05 +0200 Subject: [PATCH 36/83] bump houdini addon version --- server_addon/houdini/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 6232f7ab18..5635676f6b 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.2.10" +__version__ = "0.2.11" From b8f89e141c71327d032e1463ab8d3f523c5ceeb2 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 3 Jan 2024 14:51:35 +0200 Subject: [PATCH 37/83] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20Minkiu=20Comment?= =?UTF-8?q?=20-=20refactor=20repeated=20code=20into=20a=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/set_default_display_and_view.py | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/houdini/hooks/set_default_display_and_view.py b/openpype/hosts/houdini/hooks/set_default_display_and_view.py index bb5f8bf71a..3462ab1647 100644 --- a/openpype/hosts/houdini/hooks/set_default_display_and_view.py +++ b/openpype/hosts/houdini/hooks/set_default_display_and_view.py @@ -39,29 +39,20 @@ class SetDefaultDisplayView(PreLaunchHook): default_display = houdini_color_settings["default_display"] if default_display: # get 'OCIO_ACTIVE_DISPLAYS' value if exists. - OCIO_ACTIVE_DISPLAYS = self.launch_context.env.get( - "OCIO_ACTIVE_DISPLAYS", "" - ) - default_display = ":".join( - key for key in [default_display, OCIO_ACTIVE_DISPLAYS] if key - ) - self.log.info( - "Setting OCIO_ACTIVE_DISPLAYS environment to: {}" - .format(default_display) - ) - self.launch_context.env["OCIO_ACTIVE_DISPLAYS"] = default_display + self._set_context_env("OCIO_ACTIVE_DISPLAYS", default_display) default_view = houdini_color_settings["default_view"] if default_view: # get 'OCIO_ACTIVE_VIEWS' value if exists. - OCIO_ACTIVE_VIEWS = self.launch_context.env.get( - "OCIO_ACTIVE_VIEWS", "" + self._set_context_env("OCIO_ACTIVE_VIEWS", default_view) + + def _set_context_env(self, env_var, default_value): + env_value = self.launch_context.env.get(env_var, "") + new_value = ":".join( + key for key in [default_value, env_value] if key ) - default_view = ":".join( - key for key in [default_view, OCIO_ACTIVE_VIEWS] if key + self.log.info( + "Setting {} environment to: {}" + .format(env_var, new_value) ) - self.log.info( - "Setting OCIO_ACTIVE_VIEWS environment to: {}" - .format(default_view) - ) - self.launch_context.env["OCIO_ACTIVE_VIEWS"] = default_view + self.launch_context.env[env_var] = new_value From 71fe47dc411fa3a0a50fc951b807f868986d7e5f Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 3 Jan 2024 14:54:14 +0200 Subject: [PATCH 38/83] =?UTF-8?q?=F0=9F=A6=B4=20Resolve=20Hound?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../houdini/hooks/set_default_display_and_view.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/hooks/set_default_display_and_view.py b/openpype/hosts/houdini/hooks/set_default_display_and_view.py index 3462ab1647..4f19dcfcd6 100644 --- a/openpype/hosts/houdini/hooks/set_default_display_and_view.py +++ b/openpype/hosts/houdini/hooks/set_default_display_and_view.py @@ -49,10 +49,10 @@ class SetDefaultDisplayView(PreLaunchHook): def _set_context_env(self, env_var, default_value): env_value = self.launch_context.env.get(env_var, "") new_value = ":".join( - key for key in [default_value, env_value] if key - ) + key for key in [default_value, env_value] if key + ) self.log.info( - "Setting {} environment to: {}" - .format(env_var, new_value) - ) + "Setting {} environment to: {}" + .format(env_var, new_value) + ) self.launch_context.env[env_var] = new_value From ec6a8b2a96eae0813e9a853495d4dfc813562254 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 5 Jan 2024 16:21:21 +0200 Subject: [PATCH 39/83] =?UTF-8?q?=E2=9E=96=20Remove=20unnecessary=20functi?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openpype/hosts/houdini/api/lib.py | 36 ++++--------------- .../houdini/plugins/create/create_review.py | 16 ++++++++- .../publish/validate_review_colorspace.py | 24 ++++++++----- 3 files changed, 37 insertions(+), 39 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 4f39677afc..89e5309f29 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -866,35 +866,17 @@ def get_current_context_template_data_with_asset_data(): return template_data -def get_houdini_color_settings(): - """Get Houdini working file color settings. - - Returns: - Dict: The dictionary contains the Houdini working file color settings - if the settings are enabled, otherwise it is an empty dictionary. - """ - - project_settings = get_current_project_settings() - color_settings = project_settings["houdini"]["imageio"]["workfile"] - - # Remove leading, and trailing whitespaces - color_settings["review_color_space"] = \ - color_settings["review_color_space"].strip() - return color_settings - - -def set_review_color_space(opengl_node, log=None): +def set_review_color_space(opengl_node, review_color_space="", log=None): """Set ociocolorspace parameter for the given OpenGL node. - If workfile settings are enabled, it will use the value - exposed in the settings. - - If the value exposed in the settings is empty, - it will use the default colorspace corresponding to - the display & view of the current Houdini session. + Set `ociocolorspace` parameter of the given OpenGl node + to to the given review_color_space value. + If review_color_space is empty, a default colorspace corresponding to + the display & view of the current Houdini session will be used. Args: - OpenGl node (hou.Node): ROP node to set its ociocolorspace parameter. + opengl_node (hou.Node): ROP node to set its ociocolorspace parameter. + review_color_space (str): Colorspace value for ociocolorspace parameter. log (logging.Logger): Logger to log to. """ @@ -910,10 +892,6 @@ def set_review_color_space(opengl_node, log=None): " 'OpenColorIO'".format(opengl_node.path()) ) - # Get review color space - color_settings = get_houdini_color_settings() - review_color_space = color_settings["review_color_space"] if color_settings["enabled"] else "" # noqa - # fall to default review color space if the setting is empty. if not review_color_space: from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa diff --git a/openpype/hosts/houdini/plugins/create/create_review.py b/openpype/hosts/houdini/plugins/create/create_review.py index 775babbab6..22a5dbd817 100644 --- a/openpype/hosts/houdini/plugins/create/create_review.py +++ b/openpype/hosts/houdini/plugins/create/create_review.py @@ -2,6 +2,7 @@ """Creator plugin for creating openGL reviews.""" from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef, BoolDef, NumberDef +from openpype.settings import get_current_project_settings from openpype.hosts.houdini.api.lib import set_review_color_space import os import hou @@ -14,6 +15,11 @@ class CreateReview(plugin.HoudiniCreator): label = "Review" family = "review" icon = "video-camera" + workfile_color_settings = {} + + def apply_settings(self, project_settings): + super(CreateReview, self).apply_settings(project_settings) + self.workfile_color_settings = project_settings["houdini"]["imageio"]["workfile"] def create(self, subset_name, instance_data, pre_create_data): @@ -87,7 +93,15 @@ class CreateReview(plugin.HoudiniCreator): # Set OCIO Colorspace to the default output colorspace # if there's OCIO if os.getenv("OCIO"): - set_review_color_space(instance_node, log=self.log) + workfile_color_settings = self.project_settings["houdini"]["imageio"]["workfile"] + review_color_space = workfile_color_settings["enabled"] and \ + workfile_color_settings["review_color_space"] + + set_review_color_space( + instance_node, + review_color_space, + log=self.log) + to_lock = ["id", "family"] diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index db6533d4ec..528f4ae242 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -6,10 +6,7 @@ from openpype.pipeline import ( ) from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api.action import SelectROPAction -from openpype.hosts.houdini.api.lib import ( - get_houdini_color_settings, - set_review_color_space -) +from openpype.hosts.houdini.api.lib import set_review_color_space import os import hou @@ -65,10 +62,11 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, .format(rop_node.path()) ) - color_settings = get_houdini_color_settings() - # skip if houdini color settings are disabled - if color_settings["enabled"]: - review_color_space = color_settings["review_color_space"] + workfile_color_settings = instance.context.data["project_settings"]["houdini"]["imageio"]["workfile"] # noqa + # skip if houdini workfile color settings are disabled + if workfile_color_settings["enabled"]: + review_color_space = workfile_color_settings["review_color_space"] + # skip if review color space setting is empty. if review_color_space and \ rop_node.evalParm("ociocolorspace") != review_color_space: @@ -94,4 +92,12 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, """ opengl_node = hou.node(instance.data["instance_node"]) - set_review_color_space(opengl_node, log=cls.log) + + workfile_color_settings = instance.context.data["project_settings"]["houdini"]["imageio"]["workfile"] # noqa + review_color_space = workfile_color_settings["enabled"] and \ + workfile_color_settings["review_color_space"] + + set_review_color_space( + opengl_node, + review_color_space, + log=cls.log) From 10cdd11270aa837d45e404d60ffc4ae8da3e5fe6 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 5 Jan 2024 16:24:34 +0200 Subject: [PATCH 40/83] =?UTF-8?q?=F0=9F=90=BE=20Resolve=20Hound?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openpype/hosts/houdini/api/lib.py | 4 ++-- openpype/hosts/houdini/plugins/create/create_review.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 89e5309f29..51d490adc9 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -875,8 +875,8 @@ def set_review_color_space(opengl_node, review_color_space="", log=None): the display & view of the current Houdini session will be used. Args: - opengl_node (hou.Node): ROP node to set its ociocolorspace parameter. - review_color_space (str): Colorspace value for ociocolorspace parameter. + opengl_node (hou.Node): ROP node to set its ociocolorspace parm. + review_color_space (str): Colorspace value for ociocolorspace parm. log (logging.Logger): Logger to log to. """ diff --git a/openpype/hosts/houdini/plugins/create/create_review.py b/openpype/hosts/houdini/plugins/create/create_review.py index 22a5dbd817..8e08c76c9a 100644 --- a/openpype/hosts/houdini/plugins/create/create_review.py +++ b/openpype/hosts/houdini/plugins/create/create_review.py @@ -93,7 +93,7 @@ class CreateReview(plugin.HoudiniCreator): # Set OCIO Colorspace to the default output colorspace # if there's OCIO if os.getenv("OCIO"): - workfile_color_settings = self.project_settings["houdini"]["imageio"]["workfile"] + workfile_color_settings = self.project_settings["houdini"]["imageio"]["workfile"] # noqa review_color_space = workfile_color_settings["enabled"] and \ workfile_color_settings["review_color_space"] From eb123e1fc028a5facca643df685bd6da12f8e81a Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 5 Jan 2024 16:25:16 +0200 Subject: [PATCH 41/83] =?UTF-8?q?=F0=9F=90=BE=20Resolve=20Hound?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openpype/hosts/houdini/plugins/create/create_review.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_review.py b/openpype/hosts/houdini/plugins/create/create_review.py index 8e08c76c9a..f758b8529f 100644 --- a/openpype/hosts/houdini/plugins/create/create_review.py +++ b/openpype/hosts/houdini/plugins/create/create_review.py @@ -2,7 +2,6 @@ """Creator plugin for creating openGL reviews.""" from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef, BoolDef, NumberDef -from openpype.settings import get_current_project_settings from openpype.hosts.houdini.api.lib import set_review_color_space import os import hou From 05948a968a70db8f81774af62a6ba3f306309a70 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 5 Jan 2024 16:26:30 +0200 Subject: [PATCH 42/83] =?UTF-8?q?=F0=9F=90=BE=20Resolve=20Hound?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openpype/hosts/houdini/plugins/create/create_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_review.py b/openpype/hosts/houdini/plugins/create/create_review.py index f758b8529f..6c82afc4e5 100644 --- a/openpype/hosts/houdini/plugins/create/create_review.py +++ b/openpype/hosts/houdini/plugins/create/create_review.py @@ -18,7 +18,7 @@ class CreateReview(plugin.HoudiniCreator): def apply_settings(self, project_settings): super(CreateReview, self).apply_settings(project_settings) - self.workfile_color_settings = project_settings["houdini"]["imageio"]["workfile"] + self.workfile_color_settings = project_settings["houdini"]["imageio"]["workfile"] # noqa def create(self, subset_name, instance_data, pre_create_data): From 75b6dfe5efc606ed123531faf08d0300c6f838c2 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 16 Jan 2024 12:08:44 +0200 Subject: [PATCH 43/83] bump Houdini addon version --- server_addon/houdini/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 5635676f6b..b5c9b6cb71 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.2.11" +__version__ = "0.2.12" From 52e88951f4ced27620c6f34a220bc5ce0c622d17 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 21 Feb 2024 11:53:41 +0100 Subject: [PATCH 44/83] Traypublisher CSV ingest ayon conversion kickof Traypublisher CSV ingest ayon conversion kickoff Added functionality for ingesting CSV files into projects. Includes commands to ingest CSV data, publish the content, and create instances based on the CSV data. --- client/ayon_core/hosts/traypublisher/addon.py | 61 +- .../hosts/traypublisher/csv_publish.py | 76 ++ .../plugins/create/create_csv_ingest.py | 686 ++++++++++++++++++ .../collect_csv_ingest_instance_data.py | 36 + .../plugins/publish/extract_csv_file.py | 31 + .../publish/validate_existing_version.py | 1 + .../plugins/publish/validate_frame_ranges.py | 2 + client/ayon_core/plugins/publish/integrate.py | 3 +- 8 files changed, 894 insertions(+), 2 deletions(-) create mode 100644 client/ayon_core/hosts/traypublisher/csv_publish.py create mode 100644 client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py create mode 100644 client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py create mode 100644 client/ayon_core/hosts/traypublisher/plugins/publish/extract_csv_file.py diff --git a/client/ayon_core/hosts/traypublisher/addon.py b/client/ayon_core/hosts/traypublisher/addon.py index 70bdfe9a64..f3884aedfe 100644 --- a/client/ayon_core/hosts/traypublisher/addon.py +++ b/client/ayon_core/hosts/traypublisher/addon.py @@ -1,6 +1,6 @@ import os -from ayon_core.lib import get_ayon_launcher_args +from pathlib import Path from ayon_core.lib.execute import run_detached_process from ayon_core.addon import ( click_wrap, @@ -57,3 +57,62 @@ def launch(): from ayon_core.tools import traypublisher traypublisher.main() + + +@cli_main.command() +@click_wrap.option( + "--csv-filepath", + help="Full path to CSV file with data", + type=str, + required=True +) +@click_wrap.option( + "--project-name", + help="Project name in which the context will be used", + type=str, + required=True +) +@click_wrap.option( + "--asset-name", + help="Asset name in which the context will be used", + type=str, + required=True +) +@click_wrap.option( + "--task-name", + help="Task name under Asset in which the context will be used", + type=str, + required=False +) +@click_wrap.option( + "--ignore-validators", + help="Option to ignore validators", + type=bool, + is_flag=True, + required=False +) +def ingestcsv( + csv_filepath, + project_name, + asset_name, + task_name, + ignore_validators +): + """Ingest CSV file into project. + + This command will ingest CSV file into project. CSV file must be in + specific format. See documentation for more information. + """ + from .csv_publish import csvpublish + + # use Path to check if csv_filepath exists + if not Path(csv_filepath).exists(): + raise FileNotFoundError(f"File {csv_filepath} does not exist.") + + csvpublish( + csv_filepath, + project_name, + asset_name, + task_name, + ignore_validators + ) diff --git a/client/ayon_core/hosts/traypublisher/csv_publish.py b/client/ayon_core/hosts/traypublisher/csv_publish.py new file mode 100644 index 0000000000..f8eed2f2c5 --- /dev/null +++ b/client/ayon_core/hosts/traypublisher/csv_publish.py @@ -0,0 +1,76 @@ +import os + +import pyblish.api +import pyblish.util + +from ayon_core.client import get_asset_by_name +from ayon_core.lib.attribute_definitions import FileDefItem +from ayon_core.pipeline import install_host +from ayon_core.pipeline.create import CreateContext + +from ayon_core.hosts.traypublisher.api import TrayPublisherHost + + +def csvpublish( + csv_filepath, + project_name, + asset_name, + task_name=None, + ignore_validators=False +): + """Publish CSV file. + + Args: + csv_filepath (str): Path to CSV file. + project_name (str): Project name. + asset_name (str): Asset name. + task_name (Optional[str]): Task name. + ignore_validators (Optional[bool]): Option to ignore validators. + """ + + # initialization of host + host = TrayPublisherHost() + install_host(host) + + # setting host context into project + host.set_project_name(project_name) + + # add asset context to environment + # TODO: perhaps this can be done in a better way? + os.environ.update({ + "AVALON_PROJECT": project_name, + "AVALON_ASSET": asset_name, + "AVALON_TASK": task_name or "" + }) + + # form precreate data with field values + file_field = FileDefItem.from_paths([csv_filepath], False).pop().to_dict() + precreate_data = { + "csv_filepath_data": file_field, + } + + # create context initialization + create_context = CreateContext(host, headless=True) + asset_doc = get_asset_by_name( + project_name, + asset_name + ) + + create_context.create( + "io.openpype.creators.traypublisher.csv_ingest", + "Main", + asset_doc=asset_doc, + task_name=task_name, + pre_create_data=precreate_data, + ) + + # publishing context initialization + pyblish_context = pyblish.api.Context() + pyblish_context.data["create_context"] = create_context + + # redefine targets (skip 'local' to disable validators) + if ignore_validators: + targets = ["default", "ingest"] + + # publishing + pyblish.util.publish(context=pyblish_context, targets=targets) diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py new file mode 100644 index 0000000000..aa986657dc --- /dev/null +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py @@ -0,0 +1,686 @@ +import os +import re +import csv +import clique +from copy import deepcopy, copy + +from ayon_core.client import get_asset_by_name +from ayon_core.pipeline.create import get_subset_name +from ayon_core.pipeline import CreatedInstance +from ayon_core.lib import FileDef, BoolDef +from ayon_core.lib.transcoding import ( + VIDEO_EXTENSIONS, IMAGE_EXTENSIONS +) + +from ayon_core.hosts.traypublisher.api.plugin import ( + TrayPublishCreator +) + + +class IngestCSV(TrayPublishCreator): + """CSV ingest creator class""" + + icon = "fa.file" + + label = "CSV Ingest" + family = "csv_ingest_file" + identifier = "io.ayon_core.creators.traypublisher.csv_ingest" + + default_variants = ["Main"] + + description = "Ingest products' data from CSV file" + detailed_description = """ +Ingest products' data from CSV file following column and representation +configuration in project settings. +""" + + # Position in the list of creators. + order = 10 + + # settings for this creator + columns_config = {} + representations_config = {} + + + def create(self, subset_name, instance_data, pre_create_data): + """Create an product from each row found in the CSV. + + Args: + subset_name (str): The subset name. + instance_data (dict): The instance data. + pre_create_data (dict): + """ + + csv_filepath_data = pre_create_data.get("csv_filepath_data", {}) + + folder = csv_filepath_data.get("directory", "") + if not os.path.exists(folder): + raise FileNotFoundError( + f"Directory '{folder}' does not exist." + ) + filename = csv_filepath_data.get("filenames", []) + self._process_csv_file(subset_name, instance_data, folder, filename[0]) + + def _process_csv_file( + self, subset_name, instance_data, staging_dir, filename): + """Process CSV file. + + Args: + subset_name (str): The subset name. + instance_data (dict): The instance data. + staging_dir (str): The staging directory. + filename (str): The filename. + """ + + # create new instance from the csv file via self function + self._pass_data_to_csv_instance( + instance_data, + staging_dir, + filename + ) + + csv_instance = CreatedInstance( + self.family, subset_name, instance_data, self + ) + self._store_new_instance(csv_instance) + + csv_instance["csvFileData"] = { + "filename": filename, + "staging_dir": staging_dir, + } + + # from special function get all data from csv file and convert them + # to new instances + csv_data_for_instances = self._get_data_from_csv( + staging_dir, filename) + + # create instances from csv data via self function + self._create_instances_from_csv_data( + csv_data_for_instances, staging_dir + ) + + def _create_instances_from_csv_data( + self, + csv_data_for_instances, + staging_dir + ): + """Create instances from csv data""" + + for asset_name, _data in csv_data_for_instances.items(): + asset_doc = _data["asset_doc"] + products = _data["products"] + + for instance_name, product_data in products.items(): + # get important instance variables + task_name = product_data["task_name"] + variant = product_data["variant"] + product_type = product_data["product_type"] + version = product_data["version"] + + # create subset/product name + product_name = get_subset_name( + product_type, + variant, + task_name, + asset_doc, + ) + + # make sure frame start/end is inherited from csv columns + # expected frame range data are handles excluded + for _, repre_data in product_data["representations"].items(): # noqa: E501 + frame_start = repre_data["frameStart"] + frame_end = repre_data["frameEnd"] + handle_start = repre_data["handleStart"] + handle_end = repre_data["handleEnd"] + fps = repre_data["fps"] + break + + # try to find any version comment in representation data + version_comment = next( + iter( + repre_data["comment"] + for _, repre_data in product_data["representations"].items() # noqa: E501 + if repre_data["comment"] + ), + None + ) + + # try to find any slate switch in representation data + slate_exists = any( + repre_data["slate"] + for _, repre_data in product_data["representations"].items() # noqa: E501 + ) + + # get representations from product data + representations = product_data["representations"] + label = f"{asset_name}_{product_name}_v{version:>03}" + + families = ["csv_ingest"] + if slate_exists: + # adding slate to families mainly for loaders to be able + # to filter out slates + families.append("slate") + + # make product data + product_data = { + "name": instance_name, + "asset": asset_name, + "families": families, + "label": label, + "task": task_name, + "variant": variant, + "source": "csv", + "frameStart": frame_start, + "frameEnd": frame_end, + "handleStart": handle_start, + "handleEnd": handle_end, + "fps": fps, + "version": version, + "comment": version_comment, + } + + # create new instance + new_instance = CreatedInstance( + product_type, product_name, product_data, self + ) + self._store_new_instance(new_instance) + + if not new_instance.get("prepared_data_for_repres"): + new_instance["prepared_data_for_repres"] = [] + + base_thumbnail_repre_data = { + "name": "thumbnail", + "ext": None, + "files": None, + "stagingDir": None, + "stagingDir_persistent": True, + "tags": ["thumbnail", "delete"], + } + # need to populate all thumbnails for all representations + # so we can check if unique thumbnail per representation + # is needed + thumbnails = [ + repre_data["thumbnailPath"] + for repre_data in representations.values() + if repre_data["thumbnailPath"] + ] + multiple_thumbnails = len(set(thumbnails)) > 1 + explicit_output_name = None + thumbnails_processed = False + for filepath, repre_data in representations.items(): + # check if any review derivate tag is present + reviewable = any( + tag for tag in repre_data.get("tags", []) + # tag can be `ftrackreview` or `review` + if "review" in tag + ) + # since we need to populate multiple thumbnails as + # representation with outputName for (Ftrack instance + # integrator) pairing with reviewable video representations + if ( + thumbnails + and multiple_thumbnails + and reviewable + ): + # multiple unique thumbnails per representation needs + # grouping by outputName + # mainly used in Ftrack instance integrator + explicit_output_name = repre_data["representationName"] + relative_thumbnail_path = repre_data["thumbnailPath"] + # representation might not have thumbnail path + # so ignore this one + if not relative_thumbnail_path: + continue + thumb_dir, thumb_file = \ + self._get_refactor_thumbnail_path( + staging_dir, relative_thumbnail_path) + filename, ext = os.path.splitext(thumb_file) + thumbnail_repr_data = deepcopy( + base_thumbnail_repre_data) + thumbnail_repr_data.update({ + "name": "thumbnail_{}".format(filename), + "ext": ext[1:], + "files": thumb_file, + "stagingDir": thumb_dir, + "outputName": explicit_output_name, + }) + new_instance["prepared_data_for_repres"].append( + ("_thumbnail_", thumbnail_repr_data) + ) + elif ( + thumbnails + and not multiple_thumbnails + and not thumbnails_processed + or not reviewable + ): + if not thumbnails: + continue + # here we will use only one thumbnail for + # all representations + relative_thumbnail_path = repre_data["thumbnailPath"] + if not relative_thumbnail_path: + relative_thumbnail_path = thumbnails.pop() + thumb_dir, thumb_file = \ + self._get_refactor_thumbnail_path( + staging_dir, relative_thumbnail_path) + _, ext = os.path.splitext(thumb_file) + thumbnail_repr_data = deepcopy( + base_thumbnail_repre_data) + thumbnail_repr_data.update({ + "ext": ext[1:], + "files": thumb_file, + "stagingDir": thumb_dir + }) + new_instance["prepared_data_for_repres"].append( + ("_thumbnail_", thumbnail_repr_data) + ) + thumbnails_processed = True + + # get representation data + representation_data = self._get_representation_data( + filepath, repre_data, staging_dir, + explicit_output_name + ) + + new_instance["prepared_data_for_repres"].append( + (repre_data["colorspace"], representation_data) + ) + + def _get_refactor_thumbnail_path( + self, staging_dir, relative_thumbnail_path): + thumbnail_abs_path = os.path.join( + staging_dir, relative_thumbnail_path) + return os.path.split( + thumbnail_abs_path) + + def _get_representation_data( + self, filepath, repre_data, staging_dir, explicit_output_name=None + ): + """Get representation data + + Args: + filepath (str): Filepath to representation file. + repre_data (dict): Representation data from CSV file. + staging_dir (str): Staging directory. + explicit_output_name (Optional[str]): Explicit output name. + For grouping purposes with reviewable components. + Defaults to None. + """ + + # get extension of file + basename = os.path.basename(filepath) + _, extension = os.path.splitext(filepath) + + # validate filepath is having correct extension based on output + config_repre_data = self.representations_config["representations"] + repre_name = repre_data["representationName"] + if repre_name not in config_repre_data: + raise KeyError( + f"Representation '{repre_name}' not found " + "in config representation data." + ) + validate_extensions = config_repre_data[repre_name]["extensions"] + if extension not in validate_extensions: + raise TypeError( + f"File extension '{extension}' not valid for " + f"output '{validate_extensions}'." + ) + + is_sequence = (extension in IMAGE_EXTENSIONS) + # convert ### string in file name to %03d + # this is for correct frame range validation + # example: file.###.exr -> file.%03d.exr + if "#" in basename: + padding = len(basename.split("#")) - 1 + basename = basename.replace("#" * padding, f"%0{padding}d") + is_sequence = True + + # make absolute path to file + absfilepath = os.path.normpath(os.path.join(staging_dir, filepath)) + dirname = os.path.dirname(absfilepath) + + # check if dirname exists + if not os.path.isdir(dirname): + raise NotADirectoryError( + f"Directory '{dirname}' does not exist." + ) + + # collect all data from dirname + paths_for_collection = [] + for file in os.listdir(dirname): + filepath = os.path.join(dirname, file) + paths_for_collection.append(filepath) + + collections, _ = clique.assemble(paths_for_collection) + + if collections: + collections = collections[0] + else: + if is_sequence: + raise ValueError( + f"No collections found in directory '{dirname}'." + ) + + frame_start = None + frame_end = None + if is_sequence: + files = [os.path.basename(file) for file in collections] + frame_start = list(collections.indexes)[0] + frame_end = list(collections.indexes)[-1] + else: + files = basename + + tags = deepcopy(repre_data["tags"]) + # if slate in repre_data is True then remove one frame from start + if repre_data["slate"]: + tags.append("has_slate") + + # get representation data + representation_data = { + "name": repre_name, + "ext": extension[1:], + "files": files, + "stagingDir": dirname, + "stagingDir_persistent": True, + "tags": tags, + } + if extension in VIDEO_EXTENSIONS: + representation_data.update({ + "fps": repre_data["fps"], + "outputName": repre_name, + }) + + if explicit_output_name: + representation_data["outputName"] = explicit_output_name + + if frame_start: + representation_data["frameStart"] = frame_start + if frame_end: + representation_data["frameEnd"] = frame_end + + return representation_data + + def _get_data_from_csv( + self, package_dir, filename + ): + """Generate instances from the csv file""" + # get current project name and code from context.data + project_name = self.create_context.get_current_project_name() + + csv_file_path = os.path.join( + package_dir, filename + ) + + # make sure csv file contains columns from following list + required_columns = [ + name for name, value in self.columns_config["columns"].items() + if value["required"] + ] + # get data from csv file + with open(csv_file_path, "r") as csv_file: + csv_reader = csv.DictReader( + csv_file, delimiter=self.columns_config["csv_delimiter"]) + + # fix fieldnames + # sometimes someone can keep extra space at the start or end of + # the column name + all_columns = [ + " ".join(column.rsplit()) for column in csv_reader.fieldnames] + # return back fixed fieldnames + csv_reader.fieldnames = all_columns + + # check if csv file contains all required columns + if any(column not in all_columns for column in required_columns): + raise KeyError( + f"Missing required columns: {required_columns}" + ) + + csv_data = {} + # get data from csv file + for row in csv_reader: + # Get required columns first + context_asset_name = self._get_row_value_with_validation( + "Folder Context", row) + task_name = self._get_row_value_with_validation( + "Task Name", row) + version = self._get_row_value_with_validation( + "Version", row) + + # Get optional columns + variant = self._get_row_value_with_validation( + "Variant", row) + product_type = self._get_row_value_with_validation( + "Product Type", row) + + pre_product_name = ( + f"{task_name}{variant}{product_type}" + f"{version}".replace(" ", "").lower() + ) + + # get representation data + filename, representation_data = \ + self._get_representation_row_data(row) + + # get all csv data into one dict and make sure there are no + # duplicates data are already validated and sorted under + # correct existing asset also check if asset exists and if + # task name is valid task in asset doc and representations + # are distributed under products following variants + if context_asset_name not in csv_data: + asset_doc = get_asset_by_name( + project_name, context_asset_name) + + # make sure asset exists + if not asset_doc: + raise ValueError( + f"Asset '{context_asset_name}' not found." + ) + # check if task name is valid task in asset doc + if task_name not in asset_doc["data"]["tasks"]: + raise ValueError( + f"Task '{task_name}' not found in asset doc." + ) + + csv_data[context_asset_name] = { + "asset_doc": asset_doc, + "products": { + pre_product_name: { + "task_name": task_name, + "variant": variant, + "product_type": product_type, + "version": version, + "representations": { + filename: representation_data, + }, + } + } + } + else: + asset_doc = csv_data[context_asset_name]["asset_doc"] + csv_products = csv_data[context_asset_name]["products"] + if pre_product_name not in csv_products: + csv_products[pre_product_name] = { + "task_name": task_name, + "variant": variant, + "product_type": product_type, + "version": version, + "representations": { + filename: representation_data, + }, + } + else: + csv_representations = \ + csv_products[pre_product_name]["representations"] + if filename in csv_representations: + raise ValueError( + f"Duplicate filename '{filename}' in csv file." + ) + csv_representations[filename] = representation_data + + return csv_data + + def _get_representation_row_data(self, row_data): + """Get representation row data""" + # Get required columns first + file_path = self._get_row_value_with_validation( + "File Path", row_data) + frame_start = self._get_row_value_with_validation( + "Frame Start", row_data) + frame_end = self._get_row_value_with_validation( + "Frame End", row_data) + handle_start = self._get_row_value_with_validation( + "Handle Start", row_data) + handle_end = self._get_row_value_with_validation( + "Handle End", row_data) + fps = self._get_row_value_with_validation( + "FPS", row_data) + + # Get optional columns + thumbnail_path = self._get_row_value_with_validation( + "Thumbnail", row_data) + colorspace = self._get_row_value_with_validation( + "Colorspace", row_data) + comment = self._get_row_value_with_validation( + "Version Comment", row_data) + repre = self._get_row_value_with_validation( + "Representation", row_data) + slate_exists = self._get_row_value_with_validation( + "Slate Exists", row_data) + repre_tags = self._get_row_value_with_validation( + "Representation Tags", row_data) + + # convert tags value to list + tags_list = copy(self.representations_config["default_tags"]) + if repre_tags: + tags_list = [] + tags_delimiter = self.representations_config["tags_delimiter"] + # strip spaces from repre_tags + if tags_delimiter in repre_tags: + tags = repre_tags.split(tags_delimiter) + for _tag in tags: + tags_list.append(("".join(_tag.strip())).lower()) + else: + tags_list.append(repre_tags) + + representation_data = { + "colorspace": colorspace, + "comment": comment, + "representationName": repre, + "slate": slate_exists, + "tags": tags_list, + "thumbnailPath": thumbnail_path, + "frameStart": int(frame_start), + "frameEnd": int(frame_end), + "handleStart": int(handle_start), + "handleEnd": int(handle_end), + "fps": float(fps), + } + + return file_path, representation_data + + def _get_row_value_with_validation( + self, column_name, row_data, default_value=None + ): + """Get row value with validation""" + columns_config = self.columns_config["columns"] + # get column data from column config + column_data = columns_config.get(column_name) + if not column_data: + raise KeyError( + f"Column '{column_name}' not found in column config." + ) + + # get column value from row + column_value = row_data.get(column_name) + column_required = column_data["required"] + + # check if column value is not empty string and column is required + if column_value == "" and column_required: + raise ValueError( + f"Value in column '{column_name}' is required." + ) + + # get column type + column_type = column_data["type"] + # get column validation regex + column_validation = column_data["validate"] + # get column default value + column_default = default_value or column_data["default"] + + if column_type in ["number", "decimal"] and column_default == 0: + column_default = None + + # check if column value is not empty string + if column_value == "": + # set default value if column value is empty string + column_value = column_default + + # set column value to correct type following column type + if column_type == "number" and column_value is not None: + column_value = int(column_value) + elif column_type == "decimal" and column_value is not None: + column_value = float(column_value) + elif column_type == "bool": + column_value = column_value in ["true", "True"] + + # check if column value matches validation regex + if ( + column_value is not None and + not re.match(str(column_validation), str(column_value)) + ): + raise ValueError( + f"Column '{column_name}' value '{column_value}' " + f"does not match validation regex '{column_validation}' \n" + f"Row data: {row_data} \n" + f"Column data: {column_data}" + ) + + return column_value + + def _pass_data_to_csv_instance( + self, instance_data, staging_dir, filename + ): + """Pass CSV representation file to instance data""" + + representation = { + "name": "csv", + "ext": "csv", + "files": filename, + "stagingDir": staging_dir, + "stagingDir_persistent": True, + } + + instance_data.update({ + "label": f"CSV: {filename}", + "representations": [representation], + "stagingDir": staging_dir, + "stagingDir_persistent": True, + }) + + def get_instance_attr_defs(self): + return [ + BoolDef( + "add_review_family", + default=True, + label="Review" + ) + ] + + def get_pre_create_attr_defs(self): + """Creating pre-create attributes at creator plugin. + + Returns: + list: list of attribute object instances + """ + # Use same attributes as for instance attrobites + attr_defs = [ + FileDef( + "csv_filepath_data", + folders=False, + extensions=[".csv"], + allow_sequences=False, + single_item=True, + label="CSV File", + ), + ] + return attr_defs diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py new file mode 100644 index 0000000000..0da3ebed81 --- /dev/null +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py @@ -0,0 +1,36 @@ +from pprint import pformat +import pyblish.api +from ayon_core.pipeline import publish + + +class CollectCSVIngestInstancesData( + pyblish.api.InstancePlugin, + publish.AYONPyblishPluginMixin, + publish.ColormanagedPyblishPluginMixin +): + """Collect CSV Ingest data from instance. + """ + + label = "Collect CSV Ingest instances data" + order = pyblish.api.CollectorOrder + 0.1 + hosts = ["traypublisher"] + families = ["csv_ingest"] + + def process(self, instance): + self.log.info(f"Collecting {instance.name}") + + # expecting [(colorspace, repre_data), ...] + prepared_repres_data_items = instance.data[ + "prepared_data_for_repres"] + + for colorspace, repre_data in prepared_repres_data_items: + # only apply colorspace to those which are not marked as thumbnail + if colorspace != "_thumbnail_": + # colorspace name is passed from CSV column + self.set_representation_colorspace( + repre_data, instance.context, colorspace + ) + + instance.data["representations"].append(repre_data) + + self.log.debug(pformat(instance.data)) diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/extract_csv_file.py b/client/ayon_core/hosts/traypublisher/plugins/publish/extract_csv_file.py new file mode 100644 index 0000000000..4bdf7c0493 --- /dev/null +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/extract_csv_file.py @@ -0,0 +1,31 @@ +import pyblish.api + +from ayon_core.pipeline import publish + + +class ExtractCSVFile(publish.Extractor): + """ + Extractor export CSV file + """ + + label = "Extract CSV file" + order = pyblish.api.ExtractorOrder - 0.45 + families = ["csv_ingest_file"] + hosts = ["traypublisher"] + + def process(self, instance): + + csv_file_data = instance.data["csvFileData"] + + representation_csv = { + 'name': "csv_data", + 'ext': "csv", + 'files': csv_file_data["filename"], + "stagingDir": csv_file_data["staging_dir"], + "stagingDir_persistent": True + } + + instance.data["representations"].append(representation_csv) + + self.log.info("Added CSV file representation: {}".format( + representation_csv)) diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_existing_version.py b/client/ayon_core/hosts/traypublisher/plugins/publish/validate_existing_version.py index 6a85f92ce1..7a35a19a85 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_existing_version.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/validate_existing_version.py @@ -16,6 +16,7 @@ class ValidateExistingVersion( order = ValidateContentsOrder hosts = ["traypublisher"] + targets = ["local"] actions = [RepairAction] diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_frame_ranges.py b/client/ayon_core/hosts/traypublisher/plugins/publish/validate_frame_ranges.py index cd4a98b84d..ca53a2c8ef 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_frame_ranges.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/validate_frame_ranges.py @@ -16,6 +16,8 @@ class ValidateFrameRange(OptionalPyblishPluginMixin, label = "Validate Frame Range" hosts = ["traypublisher"] families = ["render", "plate"] + targets = ["local"] + order = ValidateContentsOrder optional = True diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index a502031595..2788875c23 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -140,7 +140,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "uasset", "blendScene", "yeticacheUE", - "tycache" + "tycache", + "csv_ingest_file", ] default_template_name = "publish" From 6274e5259a8b1a0daa31fe96847ead0a6e1cea74 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 22 Feb 2024 17:15:20 +0100 Subject: [PATCH 45/83] Settings models and validators for batch movie creation and CSV ingestion plugins. Update addon version to 0.1.4. --- .../server/settings/creator_plugins.py | 295 ++++++++++++++++++ server_addon/traypublisher/server/version.py | 2 +- 2 files changed, 296 insertions(+), 1 deletion(-) diff --git a/server_addon/traypublisher/server/settings/creator_plugins.py b/server_addon/traypublisher/server/settings/creator_plugins.py index bf66d9a088..7ce241faa6 100644 --- a/server_addon/traypublisher/server/settings/creator_plugins.py +++ b/server_addon/traypublisher/server/settings/creator_plugins.py @@ -1,4 +1,7 @@ +from pydantic import validator from ayon_server.settings import BaseSettingsModel, SettingsField +from ayon_server.settings.validators import ensure_unique_names +from ayon_server.exceptions import BadRequestException class BatchMovieCreatorPlugin(BaseSettingsModel): @@ -22,11 +25,137 @@ class BatchMovieCreatorPlugin(BaseSettingsModel): ) +class ColumnItemModel(BaseSettingsModel): + """Allows to publish multiple video files in one go.
Name of matching + asset is parsed from file names ('asset.mov', 'asset_v001.mov', + 'my_asset_to_publish.mov')""" + + name: str = SettingsField( + title="Name", + default="" + ) + + type: str = SettingsField( + title="Type", + default="" + ) + + default: str = SettingsField( + title="Default", + default="" + ) + + required: bool = SettingsField( + title="Required", + default=False + ) + + validate: str = SettingsField( + title="Validate", + default="" + ) + + +class ColumnConfigModel(BaseSettingsModel): + """Allows to publish multiple video files in one go.
Name of matching + asset is parsed from file names ('asset.mov', 'asset_v001.mov', + 'my_asset_to_publish.mov')""" + + csv_delimiter: str = SettingsField( + title="CSV delimiter", + default="," + ) + + columns: list[ColumnItemModel] = SettingsField( + title="Columns", + default_factory=list + ) + + @validator("columns") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + + +class RepresentationItemModel(BaseSettingsModel): + """Allows to publish multiple video files in one go.
Name of matching + asset is parsed from file names ('asset.mov', 'asset_v001.mov', + 'my_asset_to_publish.mov')""" + + name: str = SettingsField( + title="Name", + default="" + ) + + extensions: list[str] = SettingsField( + title="Extensions", + default_factory=list + ) + + @validator("extensions") + def validate_extension(cls, value): + for ext in value: + if not ext.startswith("."): + raise BadRequestException("Extension must start with '.'") + return value + + +class RepresentationConfigModel(BaseSettingsModel): + """Allows to publish multiple video files in one go.
Name of matching + asset is parsed from file names ('asset.mov', 'asset_v001.mov', + 'my_asset_to_publish.mov')""" + + tags_delimiter: str = SettingsField( + title="Tags delimiter", + default=";" + ) + + default_tags: list[str] = SettingsField( + title="Default tags", + default_factory=list + ) + + representation: list[RepresentationItemModel] = SettingsField( + title="Representation", + default_factory=list + ) + + @validator("representation") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + + +class IngestCSVPluginModel(BaseSettingsModel): + """Allows to publish multiple video files in one go.
Name of matching + asset is parsed from file names ('asset.mov', 'asset_v001.mov', + 'my_asset_to_publish.mov')""" + + enabled: bool = SettingsField( + title="Enabled", + default=False + ) + + columns_config: ColumnConfigModel = SettingsField( + title="Columns config", + default_factory=ColumnConfigModel + ) + + representations_config: dict = SettingsField( + title="Representations config", + default_factory=dict + ) + + class TrayPublisherCreatePluginsModel(BaseSettingsModel): BatchMovieCreator: BatchMovieCreatorPlugin = SettingsField( title="Batch Movie Creator", default_factory=BatchMovieCreatorPlugin ) + IngestCSV: IngestCSVPluginModel = SettingsField( + title="Ingest CSV", + default_factory=IngestCSVPluginModel + ) DEFAULT_CREATORS = { @@ -41,4 +170,170 @@ DEFAULT_CREATORS = { ".mov" ] }, + "IngestCSV": { + "enabled": True, + "columns_config": { + "csv_delimiter": ",", + "columns": [ + { + "name": "File Path", + "type": "text", + "default": "", + "required": True, + "validate": "^([a-z0-9#._\\/]*)$" + }, + { + "name": "Folder Context", + "type": "text", + "default": "", + "required": True, + "validate": "^([a-zA-Z0-9_]*)$" + }, + { + "name": "Task Name", + "type": "text", + "default": "", + "required": True, + "validate": "^(.*)$" + }, + { + "name": "Version", + "type": "number", + "default": 1, + "required": True, + "validate": "^(\\d{1,3})$" + }, + { + "name": "Frame Start", + "type": "number", + "default": 0, + "required": True, + "validate": "^(\\d{1,8})$" + }, + { + "name": "Frame End", + "type": "number", + "default": 0, + "required": True, + "validate": "^(\\d{1,8})$" + }, + { + "name": "Handle Start", + "type": "number", + "default": 0, + "required": True, + "validate": "^(\\d)$" + }, + { + "name": "Handle End", + "type": "number", + "default": 0, + "required": True, + "validate": "^(\\d)$" + }, + { + "name": "FPS", + "type": "decimal", + "default": 0.0, + "required": True, + "validate": "^[0-9]*\\.[0-9]+$|^[0-9]+$" + }, + { + "name": "Thumbnail", + "type": "text", + "default": "", + "required": False, + "validate": "^([a-z0-9#._\\/]*)$" + }, + { + "name": "Colorspace", + "type": "text", + "default": "", + "required": False, + "validate": "^(.*)$" + }, + { + "name": "Version Comment", + "type": "text", + "default": "", + "required": False, + "validate": "^(.*)$" + }, + { + "name": "Representation", + "type": "text", + "default": "", + "required": False, + "validate": "^(.*)$" + }, + { + "name": "Product Type", + "type": "text", + "default": "", + "required": False, + "validate": "^(.*)$" + }, + { + "name": "Variant", + "type": "text", + "default": "", + "required": False, + "validate": "^(.*)$" + }, + { + "name": "Slate Exists", + "type": "bool", + "default": True, + "required": False, + "validate": "(True|False)" + }, + { + "name": "Representation Tags", + "type": "text", + "default": "", + "required": False, + "validate": "^(.*)$" + } + ] + }, + "representations_config": { + "tags_delimiter": ";", + "default_tags": [ + "review" + ], + "representations": [ + { + "name": "preview", + "extensions": [ + ".mp4", + ".mov" + ] + }, + { + "name": "exr", + "extensions": [ + ".exr" + ] + }, + { + "name": "edit", + "extensions": [ + ".mov" + ] + }, + { + "name": "review", + "extensions": [ + ".mov" + ] + }, + { + "name": "nuke", + "extensions": [ + ".nk" + ] + } + ] + } + } } diff --git a/server_addon/traypublisher/server/version.py b/server_addon/traypublisher/server/version.py index e57ad00718..de699158fd 100644 --- a/server_addon/traypublisher/server/version.py +++ b/server_addon/traypublisher/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.3" +__version__ = "0.1.4" From fe0ccb2a4126bb7f632b3ace39b9f592d1ae0fe7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 22 Feb 2024 17:15:36 +0100 Subject: [PATCH 46/83] Refactor column validation and retrieval logic. - Refactored how columns are validated and retrieved for better clarity. --- .../plugins/create/create_csv_ingest.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py index aa986657dc..a11810f902 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py @@ -312,14 +312,20 @@ configuration in project settings. _, extension = os.path.splitext(filepath) # validate filepath is having correct extension based on output - config_repre_data = self.representations_config["representations"] repre_name = repre_data["representationName"] - if repre_name not in config_repre_data: + repre_config_data = None + for repre in self.representations_config["representations"]: + if repre["name"] == repre_name: + repre_config_data = repre + break + + if not repre_config_data: raise KeyError( f"Representation '{repre_name}' not found " "in config representation data." ) - validate_extensions = config_repre_data[repre_name]["extensions"] + + validate_extensions = repre_config_data["extensions"] if extension not in validate_extensions: raise TypeError( f"File extension '{extension}' not valid for " @@ -413,8 +419,8 @@ configuration in project settings. # make sure csv file contains columns from following list required_columns = [ - name for name, value in self.columns_config["columns"].items() - if value["required"] + column["name"] for column in self.columns_config["columns"] + if column["required"] ] # get data from csv file with open(csv_file_path, "r") as csv_file: @@ -582,9 +588,14 @@ configuration in project settings. self, column_name, row_data, default_value=None ): """Get row value with validation""" - columns_config = self.columns_config["columns"] + # get column data from column config - column_data = columns_config.get(column_name) + column_data = None + for column in self.columns_config["columns"]: + if column["name"] == column_name: + column_data = column + break + if not column_data: raise KeyError( f"Column '{column_name}' not found in column config." From 5595c021e244470d936059b98e44552a3b078e29 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 22 Feb 2024 22:15:56 +0100 Subject: [PATCH 47/83] Update column attribute names and validation patterns in creator plugins settings. Improve error message for file extension validation. --- .../plugins/create/create_csv_ingest.py | 4 +- .../server/settings/creator_plugins.py | 80 +++++++++---------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py index a11810f902..7c379784f7 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py @@ -603,7 +603,7 @@ configuration in project settings. # get column value from row column_value = row_data.get(column_name) - column_required = column_data["required"] + column_required = column_data["required_column"] # check if column value is not empty string and column is required if column_value == "" and column_required: @@ -614,7 +614,7 @@ configuration in project settings. # get column type column_type = column_data["type"] # get column validation regex - column_validation = column_data["validate"] + column_validation = column_data["validation_pattern"] # get column default value column_default = default_value or column_data["default"] diff --git a/server_addon/traypublisher/server/settings/creator_plugins.py b/server_addon/traypublisher/server/settings/creator_plugins.py index 7ce241faa6..9f9c31da98 100644 --- a/server_addon/traypublisher/server/settings/creator_plugins.py +++ b/server_addon/traypublisher/server/settings/creator_plugins.py @@ -45,14 +45,14 @@ class ColumnItemModel(BaseSettingsModel): default="" ) - required: bool = SettingsField( - title="Required", + required_column: bool = SettingsField( + title="Required Column", default=False ) - validate: str = SettingsField( - title="Validate", - default="" + validation_pattern: str = SettingsField( + title="Validation Regex Pattern", + default="^(.*)$" ) @@ -96,7 +96,7 @@ class RepresentationItemModel(BaseSettingsModel): def validate_extension(cls, value): for ext in value: if not ext.startswith("."): - raise BadRequestException("Extension must start with '.'") + raise BadRequestException(f"Extension must start with '.': {ext}") return value @@ -179,120 +179,120 @@ DEFAULT_CREATORS = { "name": "File Path", "type": "text", "default": "", - "required": True, - "validate": "^([a-z0-9#._\\/]*)$" + "required_column": True, + "validation_pattern": "^([a-z0-9#._\\/]*)$" }, { "name": "Folder Context", "type": "text", "default": "", - "required": True, - "validate": "^([a-zA-Z0-9_]*)$" + "required_column": True, + "validation_pattern": "^([a-zA-Z0-9_]*)$" }, { "name": "Task Name", "type": "text", "default": "", - "required": True, - "validate": "^(.*)$" + "required_column": True, + "validation_pattern": "^(.*)$" }, { "name": "Version", "type": "number", "default": 1, - "required": True, - "validate": "^(\\d{1,3})$" + "required_column": True, + "validation_pattern": "^(\\d{1,3})$" }, { "name": "Frame Start", "type": "number", "default": 0, - "required": True, - "validate": "^(\\d{1,8})$" + "required_column": True, + "validation_pattern": "^(\\d{1,8})$" }, { "name": "Frame End", "type": "number", "default": 0, - "required": True, - "validate": "^(\\d{1,8})$" + "required_column": True, + "validation_pattern": "^(\\d{1,8})$" }, { "name": "Handle Start", "type": "number", "default": 0, - "required": True, - "validate": "^(\\d)$" + "required_column": True, + "validation_pattern": "^(\\d)$" }, { "name": "Handle End", "type": "number", "default": 0, - "required": True, - "validate": "^(\\d)$" + "required_column": True, + "validation_pattern": "^(\\d)$" }, { "name": "FPS", "type": "decimal", "default": 0.0, - "required": True, - "validate": "^[0-9]*\\.[0-9]+$|^[0-9]+$" + "required_column": True, + "validation_pattern": "^[0-9]*\\.[0-9]+$|^[0-9]+$" }, { "name": "Thumbnail", "type": "text", "default": "", - "required": False, - "validate": "^([a-z0-9#._\\/]*)$" + "required_column": False, + "validation_pattern": "^([a-z0-9#._\\/]*)$" }, { "name": "Colorspace", "type": "text", "default": "", - "required": False, - "validate": "^(.*)$" + "required_column": False, + "validation_pattern": "^(.*)$" }, { "name": "Version Comment", "type": "text", "default": "", - "required": False, - "validate": "^(.*)$" + "required_column": False, + "validation_pattern": "^(.*)$" }, { "name": "Representation", "type": "text", "default": "", - "required": False, - "validate": "^(.*)$" + "required_column": False, + "validation_pattern": "^(.*)$" }, { "name": "Product Type", "type": "text", "default": "", - "required": False, - "validate": "^(.*)$" + "required_column": False, + "validation_pattern": "^(.*)$" }, { "name": "Variant", "type": "text", "default": "", - "required": False, - "validate": "^(.*)$" + "required_column": False, + "validation_pattern": "^(.*)$" }, { "name": "Slate Exists", "type": "bool", "default": True, - "required": False, - "validate": "(True|False)" + "required_column": False, + "validation_pattern": "(True|False)" }, { "name": "Representation Tags", "type": "text", "default": "", - "required": False, - "validate": "^(.*)$" + "required_column": False, + "validation_pattern": "^(.*)$" } ] }, From b7bd29389240a518838fbb2124aae26da6cadea8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Mar 2024 17:19:51 +0100 Subject: [PATCH 48/83] Update addon and plugin files for Ayon Core hosts traypublisher. - Added import statement for `get_ayon_launcher_args`. - Renamed function call from `get_subset_name` to `get_product_name`. - Changed attribute name from `family` to `product_type` in class IngestCSV. - Updated comments and docstrings in the code. --- client/ayon_core/hosts/traypublisher/addon.py | 1 + .../plugins/create/create_csv_ingest.py | 7 +++---- .../server/settings/creator_plugins.py | 18 ++++++++++-------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/hosts/traypublisher/addon.py b/client/ayon_core/hosts/traypublisher/addon.py index f3884aedfe..ae0705cee2 100644 --- a/client/ayon_core/hosts/traypublisher/addon.py +++ b/client/ayon_core/hosts/traypublisher/addon.py @@ -1,6 +1,7 @@ import os from pathlib import Path +from ayon_core.lib import get_ayon_launcher_args from ayon_core.lib.execute import run_detached_process from ayon_core.addon import ( click_wrap, diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py index 7c379784f7..193e439581 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py @@ -5,7 +5,7 @@ import clique from copy import deepcopy, copy from ayon_core.client import get_asset_by_name -from ayon_core.pipeline.create import get_subset_name +from ayon_core.pipeline.create import get_product_name from ayon_core.pipeline import CreatedInstance from ayon_core.lib import FileDef, BoolDef from ayon_core.lib.transcoding import ( @@ -23,7 +23,7 @@ class IngestCSV(TrayPublishCreator): icon = "fa.file" label = "CSV Ingest" - family = "csv_ingest_file" + product_type = "csv_ingest_file" identifier = "io.ayon_core.creators.traypublisher.csv_ingest" default_variants = ["Main"] @@ -41,7 +41,6 @@ configuration in project settings. columns_config = {} representations_config = {} - def create(self, subset_name, instance_data, pre_create_data): """Create an product from each row found in the CSV. @@ -118,7 +117,7 @@ configuration in project settings. version = product_data["version"] # create subset/product name - product_name = get_subset_name( + product_name = get_product_name( product_type, variant, task_name, diff --git a/server_addon/traypublisher/server/settings/creator_plugins.py b/server_addon/traypublisher/server/settings/creator_plugins.py index 9f9c31da98..82c4d37739 100644 --- a/server_addon/traypublisher/server/settings/creator_plugins.py +++ b/server_addon/traypublisher/server/settings/creator_plugins.py @@ -78,9 +78,11 @@ class ColumnConfigModel(BaseSettingsModel): class RepresentationItemModel(BaseSettingsModel): - """Allows to publish multiple video files in one go.
Name of matching - asset is parsed from file names ('asset.mov', 'asset_v001.mov', - 'my_asset_to_publish.mov')""" + """Allows to publish multiple video files in one go. + + Name of matching asset is parsed from file names + ('asset.mov', 'asset_v001.mov', 'my_asset_to_publish.mov') + """ name: str = SettingsField( title="Name", @@ -115,12 +117,12 @@ class RepresentationConfigModel(BaseSettingsModel): default_factory=list ) - representation: list[RepresentationItemModel] = SettingsField( - title="Representation", + representations: list[RepresentationItemModel] = SettingsField( + title="Representations", default_factory=list ) - @validator("representation") + @validator("representations") def validate_unique_outputs(cls, value): ensure_unique_names(value) return value @@ -141,9 +143,9 @@ class IngestCSVPluginModel(BaseSettingsModel): default_factory=ColumnConfigModel ) - representations_config: dict = SettingsField( + representations_config: RepresentationConfigModel = SettingsField( title="Representations config", - default_factory=dict + default_factory=RepresentationConfigModel ) From 102866da849ecb6e36d0acbbd1743fd6af2e5973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 5 Mar 2024 13:48:33 +0100 Subject: [PATCH 49/83] Update client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py Co-authored-by: Roy Nieterau --- .../plugins/publish/collect_csv_ingest_instance_data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py index 0da3ebed81..1840dbb445 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py @@ -17,7 +17,6 @@ class CollectCSVIngestInstancesData( families = ["csv_ingest"] def process(self, instance): - self.log.info(f"Collecting {instance.name}") # expecting [(colorspace, repre_data), ...] prepared_repres_data_items = instance.data[ From 4cbeb1033aab65ecc2882e1f95e8ae909a504246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 5 Mar 2024 13:48:41 +0100 Subject: [PATCH 50/83] Update client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py Co-authored-by: Roy Nieterau --- .../plugins/publish/collect_csv_ingest_instance_data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py index 1840dbb445..f76ef93c95 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py @@ -32,4 +32,3 @@ class CollectCSVIngestInstancesData( instance.data["representations"].append(repre_data) - self.log.debug(pformat(instance.data)) From ac4e3157bce395f953fd870317cdf426f9de85d7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Mar 2024 14:43:48 +0100 Subject: [PATCH 51/83] ayon conversion fixes --- client/ayon_core/hosts/traypublisher/csv_publish.py | 10 +--------- .../plugins/create/create_csv_ingest.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/hosts/traypublisher/csv_publish.py b/client/ayon_core/hosts/traypublisher/csv_publish.py index f8eed2f2c5..6f2e335f89 100644 --- a/client/ayon_core/hosts/traypublisher/csv_publish.py +++ b/client/ayon_core/hosts/traypublisher/csv_publish.py @@ -35,14 +35,6 @@ def csvpublish( # setting host context into project host.set_project_name(project_name) - # add asset context to environment - # TODO: perhaps this can be done in a better way? - os.environ.update({ - "AVALON_PROJECT": project_name, - "AVALON_ASSET": asset_name, - "AVALON_TASK": task_name or "" - }) - # form precreate data with field values file_field = FileDefItem.from_paths([csv_filepath], False).pop().to_dict() precreate_data = { @@ -57,7 +49,7 @@ def csvpublish( ) create_context.create( - "io.openpype.creators.traypublisher.csv_ingest", + "io.ayon_core.creators.traypublisher.csv_ingest", "Main", asset_doc=asset_doc, task_name=task_name, diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py index 193e439581..dd2339392f 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py @@ -79,7 +79,7 @@ configuration in project settings. ) csv_instance = CreatedInstance( - self.family, subset_name, instance_data, self + self.product_type, subset_name, instance_data, self ) self._store_new_instance(csv_instance) @@ -106,6 +106,7 @@ configuration in project settings. """Create instances from csv data""" for asset_name, _data in csv_data_for_instances.items(): + project_name = self.create_context.get_current_project_name() asset_doc = _data["asset_doc"] products = _data["products"] @@ -118,10 +119,12 @@ configuration in project settings. # create subset/product name product_name = get_product_name( - product_type, - variant, - task_name, + project_name, asset_doc, + task_name, + self.host_name, + product_type, + variant ) # make sure frame start/end is inherited from csv columns @@ -419,7 +422,7 @@ configuration in project settings. # make sure csv file contains columns from following list required_columns = [ column["name"] for column in self.columns_config["columns"] - if column["required"] + if column["required_column"] ] # get data from csv file with open(csv_file_path, "r") as csv_file: From 09effd30fb2d22c519cd16b61b82dd46413454b4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Mar 2024 16:16:52 +0100 Subject: [PATCH 52/83] Update column names and add new fields for better data representation in CSV ingest and creator plugins. --- .../plugins/create/create_csv_ingest.py | 5 +- .../server/settings/creator_plugins.py | 66 +++++++++---------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py index dd2339392f..00ff7e00ed 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py @@ -545,9 +545,9 @@ configuration in project settings. # Get optional columns thumbnail_path = self._get_row_value_with_validation( - "Thumbnail", row_data) + "Version Thumbnail", row_data) colorspace = self._get_row_value_with_validation( - "Colorspace", row_data) + "Representation Colorspace", row_data) comment = self._get_row_value_with_validation( "Version Comment", row_data) repre = self._get_row_value_with_validation( @@ -583,7 +583,6 @@ configuration in project settings. "handleEnd": int(handle_end), "fps": float(fps), } - return file_path, representation_data def _get_row_value_with_validation( diff --git a/server_addon/traypublisher/server/settings/creator_plugins.py b/server_addon/traypublisher/server/settings/creator_plugins.py index 82c4d37739..3a07a76e6f 100644 --- a/server_addon/traypublisher/server/settings/creator_plugins.py +++ b/server_addon/traypublisher/server/settings/creator_plugins.py @@ -198,6 +198,20 @@ DEFAULT_CREATORS = { "required_column": True, "validation_pattern": "^(.*)$" }, + { + "name": "Product Type", + "type": "text", + "default": "", + "required_column": False, + "validation_pattern": "^(.*)$" + }, + { + "name": "Variant", + "type": "text", + "default": "", + "required_column": False, + "validation_pattern": "^(.*)$" + }, { "name": "Version", "type": "number", @@ -205,6 +219,20 @@ DEFAULT_CREATORS = { "required_column": True, "validation_pattern": "^(\\d{1,3})$" }, + { + "name": "Version Comment", + "type": "text", + "default": "", + "required_column": False, + "validation_pattern": "^(.*)$" + }, + { + "name": "Version Thumbnail", + "type": "text", + "default": "", + "required_column": False, + "validation_pattern": "^([a-zA-Z0-9#._\\/]*)$" + }, { "name": "Frame Start", "type": "number", @@ -241,25 +269,11 @@ DEFAULT_CREATORS = { "validation_pattern": "^[0-9]*\\.[0-9]+$|^[0-9]+$" }, { - "name": "Thumbnail", - "type": "text", - "default": "", + "name": "Slate Exists", + "type": "bool", + "default": True, "required_column": False, - "validation_pattern": "^([a-z0-9#._\\/]*)$" - }, - { - "name": "Colorspace", - "type": "text", - "default": "", - "required_column": False, - "validation_pattern": "^(.*)$" - }, - { - "name": "Version Comment", - "type": "text", - "default": "", - "required_column": False, - "validation_pattern": "^(.*)$" + "validation_pattern": "(True|False)" }, { "name": "Representation", @@ -269,26 +283,12 @@ DEFAULT_CREATORS = { "validation_pattern": "^(.*)$" }, { - "name": "Product Type", + "name": "Representation Colorspace", "type": "text", "default": "", "required_column": False, "validation_pattern": "^(.*)$" }, - { - "name": "Variant", - "type": "text", - "default": "", - "required_column": False, - "validation_pattern": "^(.*)$" - }, - { - "name": "Slate Exists", - "type": "bool", - "default": True, - "required_column": False, - "validation_pattern": "(True|False)" - }, { "name": "Representation Tags", "type": "text", From 8ecb1fa3a542b49308bbfa3c35e11de4ebed5379 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Mar 2024 16:17:08 +0100 Subject: [PATCH 53/83] Refactor representation data handling for thumbnails and media types. - Refactored how thumbnail and media representation data is processed. - Added logic to handle different types of representations based on colorspaces. --- .../plugins/create/create_csv_ingest.py | 40 ++++++++++++++----- .../collect_csv_ingest_instance_data.py | 21 ++++++++-- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py index 00ff7e00ed..720ef5ddb3 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py @@ -246,15 +246,27 @@ configuration in project settings. "stagingDir": thumb_dir, "outputName": explicit_output_name, }) - new_instance["prepared_data_for_repres"].append( - ("_thumbnail_", thumbnail_repr_data) - ) + new_instance["prepared_data_for_repres"].append({ + "type": "thumbnail", + "colorspace": None, + "representation": thumbnail_repr_data, + }) + # also add thumbnailPath for ayon to integrate + if not new_instance.get("thumbnailPath"): + new_instance["thumbnailPath"] = ( + os.path.join(thumb_dir, thumb_file) + ) elif ( thumbnails and not multiple_thumbnails and not thumbnails_processed or not reviewable ): + """ + For case where we have only one thumbnail + and not reviewable medias. This needs to be processed + only once per instance. + """ if not thumbnails: continue # here we will use only one thumbnail for @@ -273,9 +285,17 @@ configuration in project settings. "files": thumb_file, "stagingDir": thumb_dir }) - new_instance["prepared_data_for_repres"].append( - ("_thumbnail_", thumbnail_repr_data) - ) + new_instance["prepared_data_for_repres"].append({ + "type": "thumbnail", + "colorspace": None, + "representation": thumbnail_repr_data, + }) + # also add thumbnailPath for ayon to integrate + if not new_instance.get("thumbnailPath"): + new_instance["thumbnailPath"] = ( + os.path.join(thumb_dir, thumb_file) + ) + thumbnails_processed = True # get representation data @@ -284,9 +304,11 @@ configuration in project settings. explicit_output_name ) - new_instance["prepared_data_for_repres"].append( - (repre_data["colorspace"], representation_data) - ) + new_instance["prepared_data_for_repres"].append({ + "type": "media", + "colorspace": repre_data["colorspace"], + "representation": representation_data, + }) def _get_refactor_thumbnail_path( self, staging_dir, relative_thumbnail_path): diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py index f76ef93c95..33536d0854 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py @@ -22,13 +22,26 @@ class CollectCSVIngestInstancesData( prepared_repres_data_items = instance.data[ "prepared_data_for_repres"] - for colorspace, repre_data in prepared_repres_data_items: - # only apply colorspace to those which are not marked as thumbnail - if colorspace != "_thumbnail_": + for prep_repre_data in prepared_repres_data_items: + type = prep_repre_data["type"] + colorspace = prep_repre_data["colorspace"] + repre_data = prep_repre_data["representation"] + + # thumbnails should be skipped + if type == "media": # colorspace name is passed from CSV column self.set_representation_colorspace( repre_data, instance.context, colorspace ) + elif type == "media" and colorspace is None: + # TODO: implement colorspace file rules file parsing + self.log.warning( + "Colorspace is not defined in csv for following" + f" representation: {pformat(repre_data)}" + ) + pass + elif type == "thumbnail": + # thumbnails should be skipped + pass instance.data["representations"].append(repre_data) - From 8bb9070a9e3d4a775360025197076a528f5c2af4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Mar 2024 16:19:37 +0100 Subject: [PATCH 54/83] comment Improve handling of thumbnails by removing unnecessary iteration. --- .../hosts/traypublisher/plugins/create/create_csv_ingest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py index 720ef5ddb3..84ab5b72a1 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py @@ -272,6 +272,8 @@ configuration in project settings. # here we will use only one thumbnail for # all representations relative_thumbnail_path = repre_data["thumbnailPath"] + # popping last thumbnail from list since it is only one + # and we do not need to iterate again over it if not relative_thumbnail_path: relative_thumbnail_path = thumbnails.pop() thumb_dir, thumb_file = \ From 346db35546d67b38afcb9cdd09529720b6462c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 11 Mar 2024 12:17:20 +0100 Subject: [PATCH 55/83] Update client/ayon_core/hosts/traypublisher/csv_publish.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/hosts/traypublisher/csv_publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/traypublisher/csv_publish.py b/client/ayon_core/hosts/traypublisher/csv_publish.py index 6f2e335f89..c9fbbf917a 100644 --- a/client/ayon_core/hosts/traypublisher/csv_publish.py +++ b/client/ayon_core/hosts/traypublisher/csv_publish.py @@ -49,7 +49,7 @@ def csvpublish( ) create_context.create( - "io.ayon_core.creators.traypublisher.csv_ingest", + "io.ayon.creators.traypublisher.csv_ingest", "Main", asset_doc=asset_doc, task_name=task_name, From feffa4db66efa415683f3b0253de01ae773c2bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 11 Mar 2024 12:17:27 +0100 Subject: [PATCH 56/83] Update client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../hosts/traypublisher/plugins/create/create_csv_ingest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py index 84ab5b72a1..4ec6ca302a 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py @@ -24,7 +24,7 @@ class IngestCSV(TrayPublishCreator): label = "CSV Ingest" product_type = "csv_ingest_file" - identifier = "io.ayon_core.creators.traypublisher.csv_ingest" + identifier = "io.ayon.creators.traypublisher.csv_ingest" default_variants = ["Main"] From 0252e8796259221b418c4e3aa6e6e951cffe2674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 11 Mar 2024 12:22:28 +0100 Subject: [PATCH 57/83] Update client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../hosts/traypublisher/plugins/create/create_csv_ingest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py index 4ec6ca302a..31d0a022c5 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py @@ -141,7 +141,7 @@ configuration in project settings. version_comment = next( iter( repre_data["comment"] - for _, repre_data in product_data["representations"].items() # noqa: E501 + for repre_data in product_data["representations"].values() # noqa: E501 if repre_data["comment"] ), None From 638c68324139fbe365d36615ef21910a83f2fab2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Mar 2024 10:19:50 +0100 Subject: [PATCH 58/83] Update CLI options and function parameters for CSV file ingestion and publishing. Refactor variable names for clarity in functions related to file handling and context creation. --- client/ayon_core/hosts/traypublisher/addon.py | 28 +++++++++---------- .../hosts/traypublisher/csv_publish.py | 16 +++++------ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/client/ayon_core/hosts/traypublisher/addon.py b/client/ayon_core/hosts/traypublisher/addon.py index ae0705cee2..ee42784f98 100644 --- a/client/ayon_core/hosts/traypublisher/addon.py +++ b/client/ayon_core/hosts/traypublisher/addon.py @@ -62,25 +62,25 @@ def launch(): @cli_main.command() @click_wrap.option( - "--csv-filepath", + "--filepath", help="Full path to CSV file with data", type=str, required=True ) @click_wrap.option( - "--project-name", + "--project", help="Project name in which the context will be used", type=str, required=True ) @click_wrap.option( - "--asset-name", + "--folder", help="Asset name in which the context will be used", type=str, required=True ) @click_wrap.option( - "--task-name", + "--task", help="Task name under Asset in which the context will be used", type=str, required=False @@ -93,10 +93,10 @@ def launch(): required=False ) def ingestcsv( - csv_filepath, - project_name, - asset_name, - task_name, + filepath, + project, + folder, + task, ignore_validators ): """Ingest CSV file into project. @@ -107,13 +107,13 @@ def ingestcsv( from .csv_publish import csvpublish # use Path to check if csv_filepath exists - if not Path(csv_filepath).exists(): - raise FileNotFoundError(f"File {csv_filepath} does not exist.") + if not Path(filepath).exists(): + raise FileNotFoundError(f"File {filepath} does not exist.") csvpublish( - csv_filepath, - project_name, - asset_name, - task_name, + filepath, + project, + folder, + task, ignore_validators ) diff --git a/client/ayon_core/hosts/traypublisher/csv_publish.py b/client/ayon_core/hosts/traypublisher/csv_publish.py index c9fbbf917a..32c2b69371 100644 --- a/client/ayon_core/hosts/traypublisher/csv_publish.py +++ b/client/ayon_core/hosts/traypublisher/csv_publish.py @@ -3,7 +3,7 @@ import os import pyblish.api import pyblish.util -from ayon_core.client import get_asset_by_name +from ayon_api import get_folder_by_name from ayon_core.lib.attribute_definitions import FileDefItem from ayon_core.pipeline import install_host from ayon_core.pipeline.create import CreateContext @@ -12,18 +12,18 @@ from ayon_core.hosts.traypublisher.api import TrayPublisherHost def csvpublish( - csv_filepath, + filepath, project_name, - asset_name, + folder_name, task_name=None, ignore_validators=False ): """Publish CSV file. Args: - csv_filepath (str): Path to CSV file. + filepath (str): Path to CSV file. project_name (str): Project name. - asset_name (str): Asset name. + folder_name (str): Folder name. task_name (Optional[str]): Task name. ignore_validators (Optional[bool]): Option to ignore validators. """ @@ -36,16 +36,16 @@ def csvpublish( host.set_project_name(project_name) # form precreate data with field values - file_field = FileDefItem.from_paths([csv_filepath], False).pop().to_dict() + file_field = FileDefItem.from_paths([filepath], False).pop().to_dict() precreate_data = { "csv_filepath_data": file_field, } # create context initialization create_context = CreateContext(host, headless=True) - asset_doc = get_asset_by_name( + asset_doc = get_folder_by_name( project_name, - asset_name + folder_name=folder_name ) create_context.create( From 1c11e18314ff929f1fe1b9b7048a063552143088 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Mar 2024 11:11:24 +0100 Subject: [PATCH 59/83] Update CSV ingest plugin to handle folder paths and task types. Improve error handling for missing assets and tasks. Refactor data processing for better organization and validation. --- .../plugins/create/create_csv_ingest.py | 71 +++++++++++-------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py index 31d0a022c5..9d6f04ae99 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py @@ -4,14 +4,14 @@ import csv import clique from copy import deepcopy, copy -from ayon_core.client import get_asset_by_name +from ayon_api import get_folder_by_path, get_task_by_name from ayon_core.pipeline.create import get_product_name from ayon_core.pipeline import CreatedInstance from ayon_core.lib import FileDef, BoolDef from ayon_core.lib.transcoding import ( VIDEO_EXTENSIONS, IMAGE_EXTENSIONS ) - +from ayon_core.pipeline.create import CreatorError from ayon_core.hosts.traypublisher.api.plugin import ( TrayPublishCreator ) @@ -54,7 +54,7 @@ configuration in project settings. folder = csv_filepath_data.get("directory", "") if not os.path.exists(folder): - raise FileNotFoundError( + raise CreatorError( f"Directory '{folder}' does not exist." ) filename = csv_filepath_data.get("filenames", []) @@ -105,14 +105,14 @@ configuration in project settings. ): """Create instances from csv data""" - for asset_name, _data in csv_data_for_instances.items(): + for folder_path, prepared_data in csv_data_for_instances.items(): project_name = self.create_context.get_current_project_name() - asset_doc = _data["asset_doc"] - products = _data["products"] + products = prepared_data["products"] for instance_name, product_data in products.items(): # get important instance variables task_name = product_data["task_name"] + task_type = product_data["task_type"] variant = product_data["variant"] product_type = product_data["product_type"] version = product_data["version"] @@ -120,8 +120,8 @@ configuration in project settings. # create subset/product name product_name = get_product_name( project_name, - asset_doc, task_name, + task_type, self.host_name, product_type, variant @@ -155,7 +155,7 @@ configuration in project settings. # get representations from product data representations = product_data["representations"] - label = f"{asset_name}_{product_name}_v{version:>03}" + label = f"{folder_path}_{product_name}_v{version:>03}" families = ["csv_ingest"] if slate_exists: @@ -166,7 +166,7 @@ configuration in project settings. # make product data product_data = { "name": instance_name, - "asset": asset_name, + "folderPath": folder_path, "families": families, "label": label, "task": task_name, @@ -471,8 +471,10 @@ configuration in project settings. # get data from csv file for row in csv_reader: # Get required columns first - context_asset_name = self._get_row_value_with_validation( - "Folder Context", row) + # TODO: will need to be folder path in CSV + # TODO: `context_asset_name` is now `folder_path` + folder_path = self._get_row_value_with_validation( + "Folder Path", row) task_name = self._get_row_value_with_validation( "Task Name", row) version = self._get_row_value_with_validation( @@ -493,31 +495,40 @@ configuration in project settings. filename, representation_data = \ self._get_representation_row_data(row) + # TODO: batch query of all folder paths and task names + + # get folder entity from folder path + folder_entity = get_folder_by_path( + project_name, folder_path) + + # make sure asset exists + if not folder_entity: + raise CreatorError( + f"Asset '{folder_path}' not found." + ) + + # first get all tasks on the folder entity and then find + task_entity = get_task_by_name( + project_name, folder_entity["id"], task_name) + + # check if task name is valid task in asset doc + if not task_entity: + raise CreatorError( + f"Task '{task_name}' not found in asset doc." + ) + # get all csv data into one dict and make sure there are no # duplicates data are already validated and sorted under # correct existing asset also check if asset exists and if # task name is valid task in asset doc and representations # are distributed under products following variants - if context_asset_name not in csv_data: - asset_doc = get_asset_by_name( - project_name, context_asset_name) - - # make sure asset exists - if not asset_doc: - raise ValueError( - f"Asset '{context_asset_name}' not found." - ) - # check if task name is valid task in asset doc - if task_name not in asset_doc["data"]["tasks"]: - raise ValueError( - f"Task '{task_name}' not found in asset doc." - ) - - csv_data[context_asset_name] = { - "asset_doc": asset_doc, + if folder_path not in csv_data: + csv_data[folder_path] = { + "folder_entity": folder_entity, "products": { pre_product_name: { "task_name": task_name, + "task_type": task_entity["taskType"], "variant": variant, "product_type": product_type, "version": version, @@ -528,11 +539,11 @@ configuration in project settings. } } else: - asset_doc = csv_data[context_asset_name]["asset_doc"] - csv_products = csv_data[context_asset_name]["products"] + csv_products = csv_data[folder_path]["products"] if pre_product_name not in csv_products: csv_products[pre_product_name] = { "task_name": task_name, + "task_type": task_entity["taskType"], "variant": variant, "product_type": product_type, "version": version, From 175d299ccc5b20feea95242f156f7d15f6e0b132 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Mar 2024 13:12:30 +0100 Subject: [PATCH 60/83] Refactor error handling to use custom CreatorError class. - Replaced KeyError, TypeError, NotADirectoryError, ValueError with CreatorError for consistency and better error management. --- .../plugins/create/create_csv_ingest.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py index 9d6f04ae99..1381059fbb 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py @@ -346,14 +346,14 @@ configuration in project settings. break if not repre_config_data: - raise KeyError( + raise CreatorError( f"Representation '{repre_name}' not found " "in config representation data." ) validate_extensions = repre_config_data["extensions"] if extension not in validate_extensions: - raise TypeError( + raise CreatorError( f"File extension '{extension}' not valid for " f"output '{validate_extensions}'." ) @@ -373,7 +373,7 @@ configuration in project settings. # check if dirname exists if not os.path.isdir(dirname): - raise NotADirectoryError( + raise CreatorError( f"Directory '{dirname}' does not exist." ) @@ -389,7 +389,7 @@ configuration in project settings. collections = collections[0] else: if is_sequence: - raise ValueError( + raise CreatorError( f"No collections found in directory '{dirname}'." ) @@ -463,7 +463,7 @@ configuration in project settings. # check if csv file contains all required columns if any(column not in all_columns for column in required_columns): - raise KeyError( + raise CreatorError( f"Missing required columns: {required_columns}" ) @@ -555,7 +555,7 @@ configuration in project settings. csv_representations = \ csv_products[pre_product_name]["representations"] if filename in csv_representations: - raise ValueError( + raise CreatorError( f"Duplicate filename '{filename}' in csv file." ) csv_representations[filename] = representation_data @@ -633,7 +633,7 @@ configuration in project settings. break if not column_data: - raise KeyError( + raise CreatorError( f"Column '{column_name}' not found in column config." ) @@ -643,7 +643,7 @@ configuration in project settings. # check if column value is not empty string and column is required if column_value == "" and column_required: - raise ValueError( + raise CreatorError( f"Value in column '{column_name}' is required." ) @@ -675,7 +675,7 @@ configuration in project settings. column_value is not None and not re.match(str(column_validation), str(column_value)) ): - raise ValueError( + raise CreatorError( f"Column '{column_name}' value '{column_value}' " f"does not match validation regex '{column_validation}' \n" f"Row data: {row_data} \n" @@ -719,7 +719,7 @@ configuration in project settings. Returns: list: list of attribute object instances """ - # Use same attributes as for instance attrobites + # Use same attributes as for instance attributes attr_defs = [ FileDef( "csv_filepath_data", From 9090706252175d1cfcb38203214a376f1fd5a84b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Mar 2024 13:19:25 +0100 Subject: [PATCH 61/83] Refactor CSV file reading and data processing logic - Added import for StringIO - Refactored CSV file reading using StringIO - Improved handling of fieldnames and required columns detection --- .../plugins/create/create_csv_ingest.py | 196 +++++++++--------- 1 file changed, 102 insertions(+), 94 deletions(-) diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py index 1381059fbb..8143e8b45b 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py @@ -2,6 +2,7 @@ import os import re import csv import clique +from io import StringIO from copy import deepcopy, copy from ayon_api import get_folder_by_path, get_task_by_name @@ -335,7 +336,7 @@ configuration in project settings. # get extension of file basename = os.path.basename(filepath) - _, extension = os.path.splitext(filepath) + extension = os.path.splitext(filepath)[-1].lower() # validate filepath is having correct extension based on output repre_name = repre_data["representationName"] @@ -448,100 +449,92 @@ configuration in project settings. column["name"] for column in self.columns_config["columns"] if column["required_column"] ] - # get data from csv file + + # read csv file with open(csv_file_path, "r") as csv_file: - csv_reader = csv.DictReader( - csv_file, delimiter=self.columns_config["csv_delimiter"]) + csv_content = csv_file.read() - # fix fieldnames - # sometimes someone can keep extra space at the start or end of - # the column name - all_columns = [ - " ".join(column.rsplit()) for column in csv_reader.fieldnames] - # return back fixed fieldnames - csv_reader.fieldnames = all_columns + # read csv file with DictReader + csv_reader = csv.DictReader( + StringIO(csv_content), + delimiter=self.columns_config["csv_delimiter"] + ) - # check if csv file contains all required columns - if any(column not in all_columns for column in required_columns): + # fix fieldnames + # sometimes someone can keep extra space at the start or end of + # the column name + all_columns = [ + " ".join(column.rsplit()) for column in csv_reader.fieldnames] + + # return back fixed fieldnames + csv_reader.fieldnames = all_columns + + # check if csv file contains all required columns + if any(column not in all_columns for column in required_columns): + raise CreatorError( + f"Missing required columns: {required_columns}" + ) + + csv_data = {} + # get data from csv file + for row in csv_reader: + # Get required columns first + # TODO: will need to be folder path in CSV + # TODO: `context_asset_name` is now `folder_path` + folder_path = self._get_row_value_with_validation( + "Folder Path", row) + task_name = self._get_row_value_with_validation( + "Task Name", row) + version = self._get_row_value_with_validation( + "Version", row) + + # Get optional columns + variant = self._get_row_value_with_validation( + "Variant", row) + product_type = self._get_row_value_with_validation( + "Product Type", row) + + pre_product_name = ( + f"{task_name}{variant}{product_type}" + f"{version}".replace(" ", "").lower() + ) + + # get representation data + filename, representation_data = \ + self._get_representation_row_data(row) + + # TODO: batch query of all folder paths and task names + + # get folder entity from folder path + folder_entity = get_folder_by_path( + project_name, folder_path) + + # make sure asset exists + if not folder_entity: raise CreatorError( - f"Missing required columns: {required_columns}" + f"Asset '{folder_path}' not found." ) - csv_data = {} - # get data from csv file - for row in csv_reader: - # Get required columns first - # TODO: will need to be folder path in CSV - # TODO: `context_asset_name` is now `folder_path` - folder_path = self._get_row_value_with_validation( - "Folder Path", row) - task_name = self._get_row_value_with_validation( - "Task Name", row) - version = self._get_row_value_with_validation( - "Version", row) + # first get all tasks on the folder entity and then find + task_entity = get_task_by_name( + project_name, folder_entity["id"], task_name) - # Get optional columns - variant = self._get_row_value_with_validation( - "Variant", row) - product_type = self._get_row_value_with_validation( - "Product Type", row) - - pre_product_name = ( - f"{task_name}{variant}{product_type}" - f"{version}".replace(" ", "").lower() + # check if task name is valid task in asset doc + if not task_entity: + raise CreatorError( + f"Task '{task_name}' not found in asset doc." ) - # get representation data - filename, representation_data = \ - self._get_representation_row_data(row) - - # TODO: batch query of all folder paths and task names - - # get folder entity from folder path - folder_entity = get_folder_by_path( - project_name, folder_path) - - # make sure asset exists - if not folder_entity: - raise CreatorError( - f"Asset '{folder_path}' not found." - ) - - # first get all tasks on the folder entity and then find - task_entity = get_task_by_name( - project_name, folder_entity["id"], task_name) - - # check if task name is valid task in asset doc - if not task_entity: - raise CreatorError( - f"Task '{task_name}' not found in asset doc." - ) - - # get all csv data into one dict and make sure there are no - # duplicates data are already validated and sorted under - # correct existing asset also check if asset exists and if - # task name is valid task in asset doc and representations - # are distributed under products following variants - if folder_path not in csv_data: - csv_data[folder_path] = { - "folder_entity": folder_entity, - "products": { - pre_product_name: { - "task_name": task_name, - "task_type": task_entity["taskType"], - "variant": variant, - "product_type": product_type, - "version": version, - "representations": { - filename: representation_data, - }, - } - } - } - else: - csv_products = csv_data[folder_path]["products"] - if pre_product_name not in csv_products: - csv_products[pre_product_name] = { + # get all csv data into one dict and make sure there are no + # duplicates data are already validated and sorted under + # correct existing asset also check if asset exists and if + # task name is valid task in asset doc and representations + # are distributed under products following variants + if folder_path not in csv_data: + csv_data[folder_path] = { + "folder_entity": folder_entity, + "products": { + pre_product_name: { "task_name": task_name, "task_type": task_entity["taskType"], "variant": variant, @@ -551,14 +544,29 @@ configuration in project settings. filename: representation_data, }, } - else: - csv_representations = \ - csv_products[pre_product_name]["representations"] - if filename in csv_representations: - raise CreatorError( - f"Duplicate filename '{filename}' in csv file." - ) - csv_representations[filename] = representation_data + } + } + else: + csv_products = csv_data[folder_path]["products"] + if pre_product_name not in csv_products: + csv_products[pre_product_name] = { + "task_name": task_name, + "task_type": task_entity["taskType"], + "variant": variant, + "product_type": product_type, + "version": version, + "representations": { + filename: representation_data, + }, + } + else: + csv_representations = \ + csv_products[pre_product_name]["representations"] + if filename in csv_representations: + raise CreatorError( + f"Duplicate filename '{filename}' in csv file." + ) + csv_representations[filename] = representation_data return csv_data From 6a01b8b6ac6a7f3701b6be7fdfcbdd073864c2a9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Mar 2024 13:19:35 +0100 Subject: [PATCH 62/83] Update creator plugin field name from "Folder Context" to "Folder Path" and adjust validation pattern to allow slashes. --- server_addon/traypublisher/server/settings/creator_plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/traypublisher/server/settings/creator_plugins.py b/server_addon/traypublisher/server/settings/creator_plugins.py index 3a07a76e6f..1ff14002aa 100644 --- a/server_addon/traypublisher/server/settings/creator_plugins.py +++ b/server_addon/traypublisher/server/settings/creator_plugins.py @@ -185,11 +185,11 @@ DEFAULT_CREATORS = { "validation_pattern": "^([a-z0-9#._\\/]*)$" }, { - "name": "Folder Context", + "name": "Folder Path", "type": "text", "default": "", "required_column": True, - "validation_pattern": "^([a-zA-Z0-9_]*)$" + "validation_pattern": "^([a-zA-Z0-9_\\/]*)$" }, { "name": "Task Name", From 3791731b6ac2b42583f56ccc69501f39b6372f06 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 26 Mar 2024 14:45:30 +0100 Subject: [PATCH 63/83] Refactor option names and handle folder & task entities. - Renamed "--folder" to "--folder-path" for clarity. - Updated function parameters to use "folder_path" consistently. - Added error handling for non-existent folder or task paths. --- client/ayon_core/hosts/traypublisher/addon.py | 6 ++-- .../hosts/traypublisher/csv_publish.py | 32 +++++++++++++++---- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/hosts/traypublisher/addon.py b/client/ayon_core/hosts/traypublisher/addon.py index ee42784f98..3dd275f223 100644 --- a/client/ayon_core/hosts/traypublisher/addon.py +++ b/client/ayon_core/hosts/traypublisher/addon.py @@ -74,7 +74,7 @@ def launch(): required=True ) @click_wrap.option( - "--folder", + "--folder-path", help="Asset name in which the context will be used", type=str, required=True @@ -95,7 +95,7 @@ def launch(): def ingestcsv( filepath, project, - folder, + folder_path, task, ignore_validators ): @@ -113,7 +113,7 @@ def ingestcsv( csvpublish( filepath, project, - folder, + folder_path, task, ignore_validators ) diff --git a/client/ayon_core/hosts/traypublisher/csv_publish.py b/client/ayon_core/hosts/traypublisher/csv_publish.py index 32c2b69371..b43792a357 100644 --- a/client/ayon_core/hosts/traypublisher/csv_publish.py +++ b/client/ayon_core/hosts/traypublisher/csv_publish.py @@ -3,7 +3,7 @@ import os import pyblish.api import pyblish.util -from ayon_api import get_folder_by_name +from ayon_api import get_folder_by_path, get_task_by_name from ayon_core.lib.attribute_definitions import FileDefItem from ayon_core.pipeline import install_host from ayon_core.pipeline.create import CreateContext @@ -14,7 +14,7 @@ from ayon_core.hosts.traypublisher.api import TrayPublisherHost def csvpublish( filepath, project_name, - folder_name, + folder_path, task_name=None, ignore_validators=False ): @@ -23,7 +23,7 @@ def csvpublish( Args: filepath (str): Path to CSV file. project_name (str): Project name. - folder_name (str): Folder name. + folder_path (str): Folder path. task_name (Optional[str]): Task name. ignore_validators (Optional[bool]): Option to ignore validators. """ @@ -43,16 +43,34 @@ def csvpublish( # create context initialization create_context = CreateContext(host, headless=True) - asset_doc = get_folder_by_name( + folder_entity = get_folder_by_path( project_name, - folder_name=folder_name + folder_path=folder_path, ) + if not folder_entity: + ValueError( + f"Folder path '{folder_path}' doesn't " + f"exists at project '{project_name}'." + ) + + task_entity = get_task_by_name( + project_name, + folder_entity["id"], + task_name, + ) + + if not task_entity: + ValueError( + f"Task name '{task_name}' doesn't " + f"exists at folder '{folder_path}'." + ) + create_context.create( "io.ayon.creators.traypublisher.csv_ingest", "Main", - asset_doc=asset_doc, - task_name=task_name, + folder_entity=folder_entity, + task_entity=task_entity, pre_create_data=precreate_data, ) From efcf5148bd253968b79af43d68654eef66695e81 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 14:47:31 +0100 Subject: [PATCH 64/83] Maya: Yeti - Implement writing and loading user variables with a yeti cache --- client/ayon_core/hosts/maya/api/yeti.py | 101 ++++++++++++++++++ .../maya/plugins/load/load_yeti_cache.py | 41 +++++++ .../plugins/publish/collect_yeti_cache.py | 20 +++- 3 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 client/ayon_core/hosts/maya/api/yeti.py diff --git a/client/ayon_core/hosts/maya/api/yeti.py b/client/ayon_core/hosts/maya/api/yeti.py new file mode 100644 index 0000000000..1526c3a2f3 --- /dev/null +++ b/client/ayon_core/hosts/maya/api/yeti.py @@ -0,0 +1,101 @@ +from typing import List + +from maya import cmds + + +def get_yeti_user_variables(yeti_shape_node: str) -> List[str]: + """Get user defined yeti user variables for a `pgYetiMaya` shape node. + + Arguments: + yeti_shape_node (str): The `pgYetiMaya` shape node. + + Returns: + list: Attribute names (for a vector attribute it only lists the top + parent attribute, not the attribute per axis) + """ + + attrs = cmds.listAttr(yeti_shape_node, + userDefined=True, + string=("yetiVariableV_*", + "yetiVariableF_*")) or [] + valid_attrs = [] + for attr in attrs: + attr_type = cmds.attributeQuery(attr, node=yeti_shape_node, + attributeType=True) + if attr.startswith("yetiVariableV_") and attr_type == "double3": + # vector + valid_attrs.append(attr) + elif attr.startswith("yetiVariableF_") and attr_type == "double": + valid_attrs.append(attr) + + return valid_attrs + + +def create_yeti_variable(yeti_shape_node: str, + attr_name: str, + value=None, + force_value: bool = False) -> bool: + """Get user defined yeti user variables for a `pgYetiMaya` shape node. + + Arguments: + yeti_shape_node (str): The `pgYetiMaya` shape node. + attr_name (str): The fully qualified yeti variable name, e.g. + "yetiVariableF_myfloat" or "yetiVariableV_myvector" + value (object): The value to set (must match the type of the attribute) + When value is None it will ignored and not be set. + force_value (bool): Whether to set the value if the attribute already + exists or not. + + Returns: + bool: Whether the attribute value was set or not. + + """ + exists = cmds.attributeQuery(attr_name, node=yeti_shape_node, exists=True) + if not exists: + if attr_name.startswith("yetiVariableV_"): + _create_vector_yeti_user_variable(yeti_shape_node, attr_name) + if attr_name.startswith("yetiVariableF_"): + _create_float_yeti_user_variable(yeti_shape_node, attr_name) + + if value is not None and (not exists or force_value): + plug = "{}.{}".format(yeti_shape_node, attr_name) + if ( + isinstance(value, (list, tuple)) + and attr_name.startswith("yetiVariableV_") + ): + cmds.setAttr(plug, *value, type="double3") + else: + cmds.setAttr(plug, value) + + return True + return False + + +def _create_vector_yeti_user_variable(yeti_shape_node: str, attr_name: str): + if not attr_name.startswith("yetiVariableV_"): + raise ValueError("Must start with yetiVariableV_") + cmds.addAttr(yeti_shape_node, + longName=attr_name, + attributeType="double3", + cachedInternally=True, + keyable=True) + for axis in "XYZ": + cmds.addAttr(yeti_shape_node, + longName="{}{}".format(attr_name, axis), + attributeType="double", + parent=attr_name, + cachedInternally=True, + keyable=True) + + +def _create_float_yeti_user_variable(yeti_node: str, attr_name: str): + if not attr_name.startswith("yetiVariableF_"): + raise ValueError("Must start with yetiVariableF_") + + cmds.addAttr(yeti_node, + longName=attr_name, + attributeType="double", + cachedInternally=True, + softMinValue=0, + softMaxValue=100, + keyable=True) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_yeti_cache.py b/client/ayon_core/hosts/maya/plugins/load/load_yeti_cache.py index a5cd04b0f4..06f74e5107 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_yeti_cache.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_yeti_cache.py @@ -12,6 +12,7 @@ from ayon_core.pipeline import ( get_representation_path ) from ayon_core.hosts.maya.api import lib +from ayon_core.hosts.maya.api.yeti import create_yeti_variable from ayon_core.hosts.maya.api.pipeline import containerise from ayon_core.hosts.maya.api.plugin import get_load_color_for_product_type @@ -23,8 +24,19 @@ SKIP_UPDATE_ATTRS = { "viewportDensity", "viewportWidth", "viewportLength", + "renderDensity", + "renderWidth", + "renderLength", + "increaseRenderBounds" } +SKIP_ATTR_MESSAGE = ( + "Skipping updating %s.%s to %s because it " + "is considered a local overridable attribute. " + "Either set manually or the load the cache " + "anew." +) + def set_attribute(node, attr, value): """Wrapper of set attribute which ignores None values""" @@ -209,9 +221,31 @@ class YetiCacheLoader(load.LoaderPlugin): for attr, value in node_settings["attrs"].items(): if attr in SKIP_UPDATE_ATTRS: + self.log.info( + SKIP_ATTR_MESSAGE, yeti_node, attr, value + ) continue set_attribute(attr, value, yeti_node) + # Set up user defined attributes + user_variables = node_settings.get("user_variables", {}) + for attr, value in user_variables.items(): + was_value_set = create_yeti_variable( + yeti_shape_node=yeti_node, + attr_name=attr, + value=value, + # We do not want to update the + # value if it already exists so + # that any local overrides that + # may have been applied still + # persist + force_value=False + ) + if not was_value_set: + self.log.info( + SKIP_ATTR_MESSAGE, yeti_node, attr, value + ) + cmds.setAttr("{}.representation".format(container_node), repre_entity["id"], typ="string") @@ -332,6 +366,13 @@ class YetiCacheLoader(load.LoaderPlugin): for attr, value in attributes.items(): set_attribute(attr, value, yeti_node) + # Set up user defined attributes + user_variables = node_settings.get("user_variables", {}) + for attr, value in user_variables.items(): + create_yeti_variable(yeti_shape_node=yeti_node, + attr_name=attr, + value=value) + # Connect to the time node cmds.connectAttr("time1.outTime", "%s.currentTime" % yeti_node) diff --git a/client/ayon_core/hosts/maya/plugins/publish/collect_yeti_cache.py b/client/ayon_core/hosts/maya/plugins/publish/collect_yeti_cache.py index 067a7bc532..e1755e4212 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/collect_yeti_cache.py +++ b/client/ayon_core/hosts/maya/plugins/publish/collect_yeti_cache.py @@ -3,6 +3,7 @@ from maya import cmds import pyblish.api from ayon_core.hosts.maya.api import lib +from ayon_core.hosts.maya.api.yeti import get_yeti_user_variables SETTINGS = { @@ -34,7 +35,7 @@ class CollectYetiCache(pyblish.api.InstancePlugin): - "increaseRenderBounds" - "imageSearchPath" - Other information is the name of the transform and it's Colorbleed ID + Other information is the name of the transform and its `cbId` """ order = pyblish.api.CollectorOrder + 0.45 @@ -54,6 +55,16 @@ class CollectYetiCache(pyblish.api.InstancePlugin): # Get specific node attributes attr_data = {} for attr in SETTINGS: + # Ignore non-existing attributes with a warning, e.g. cbId + # if they have not been generated yet + if not cmds.attributeQuery(attr, node=shape, exists=True): + self.log.warning( + "Attribute '{}' not found on Yeti node: {}".format( + attr, shape + ) + ) + continue + current = cmds.getAttr("%s.%s" % (shape, attr)) # change None to empty string as Maya doesn't support # NoneType in attributes @@ -61,6 +72,12 @@ class CollectYetiCache(pyblish.api.InstancePlugin): current = "" attr_data[attr] = current + # Get user variable attributes + user_variable_attrs = { + attr: lib.get_attribute("{}.{}".format(shape, attr)) + for attr in get_yeti_user_variables(shape) + } + # Get transform data parent = cmds.listRelatives(shape, parent=True)[0] transform_data = {"name": parent, "cbId": lib.get_id(parent)} @@ -70,6 +87,7 @@ class CollectYetiCache(pyblish.api.InstancePlugin): "name": shape, "cbId": lib.get_id(shape), "attrs": attr_data, + "user_variables": user_variable_attrs } settings["nodes"].append(shape_data) From bf18daafe5c5d3128158b536cbd796fe856f81fa Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 17:42:00 +0100 Subject: [PATCH 65/83] Maya - Allow loading a published workfile as template --- .../maya/plugins/load/load_as_template.py | 39 +++++++++++++++++++ .../workfile/workfile_template_builder.py | 19 ++++----- 2 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 client/ayon_core/hosts/maya/plugins/load/load_as_template.py diff --git a/client/ayon_core/hosts/maya/plugins/load/load_as_template.py b/client/ayon_core/hosts/maya/plugins/load/load_as_template.py new file mode 100644 index 0000000000..a251f1c52e --- /dev/null +++ b/client/ayon_core/hosts/maya/plugins/load/load_as_template.py @@ -0,0 +1,39 @@ +from openpype.lib import ( + BoolDef +) +from openpype.pipeline import ( + load, + registered_host +) +from openpype.hosts.maya.api.workfile_template_builder import ( + MayaTemplateBuilder +) + + +class LoadAsTemplate(load.LoaderPlugin): + """Load workfile as a template """ + + families = ["workfile"] + label = "Load as template" + representations = ["ma", "mb"] + icon = "wrench" + color = "#775555" + order = 10 + + options = [ + BoolDef("keep_placeholders", + label="Keep Placeholders", + default=False), + BoolDef("create_first_version", + label="Create First Version", + default=False), + ] + + def load(self, context, name, namespace, data): + keep_placeholders = data.get("keep_placeholders", False) + create_first_version = data.get("create_first_version", False) + path = self.filepath_from_context(context) + builder = MayaTemplateBuilder(registered_host()) + builder.build_template(template_path=path, + keep_placeholders=keep_placeholders, + create_first_version=create_first_version) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 5e63ba444a..53f4bf8c32 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -498,15 +498,16 @@ class AbstractTemplateBuilder(object): process if version is created """ - template_preset = self.get_template_preset() - - if template_path is None: - template_path = template_preset["path"] - - if keep_placeholders is None: - keep_placeholders = template_preset["keep_placeholder"] - if create_first_version is None: - create_first_version = template_preset["create_first_version"] + if any(value is None for value in [template_path, + keep_placeholders, + create_first_version]): + template_preset = self.get_template_preset() + if template_path is None: + template_path = template_preset["path"] + if keep_placeholders is None: + keep_placeholders = template_preset["keep_placeholder"] + if create_first_version is None: + create_first_version = template_preset["create_first_version"] # check if first version is created created_version_workfile = False From bd2527ebe6cb1a856ae0e4882668d12f0739c56f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 17:46:23 +0100 Subject: [PATCH 66/83] Improve type hints --- .../pipeline/workfile/workfile_template_builder.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 53f4bf8c32..7faa67af04 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -16,6 +16,7 @@ import re import collections import copy from abc import ABCMeta, abstractmethod +from typing import TypedDict import six from ayon_api import ( @@ -52,6 +53,14 @@ from ayon_core.pipeline.create import ( _NOT_SET = object() +class TemplatePresetDict(TypedDict): + """Dictionary with `path`, `keep_placeholder` and `create_first_version` + settings from the template preset for current context.""" + path: str + keep_placeholder: bool + create_first_version: bool + + class TemplateNotFound(Exception): """Exception raised when template does not exist.""" pass @@ -773,7 +782,9 @@ class AbstractTemplateBuilder(object): - 'project_settings/{host name}/templated_workfile_build/profiles' Returns: - str: Path to a template file with placeholders. + TemplatePresetDict: Dictionary with `path`, `keep_placeholder` and + `create_first_version` settings from the template preset + for current context. Raises: TemplateProfileNotFound: When profiles are not filled. From ed68f16b720c05d1efef74de43eee428773a2c41 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 17:51:38 +0100 Subject: [PATCH 67/83] Fix refactor --- .../ayon_core/hosts/maya/plugins/load/load_as_template.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_as_template.py b/client/ayon_core/hosts/maya/plugins/load/load_as_template.py index a251f1c52e..5c546a1837 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_as_template.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_as_template.py @@ -1,11 +1,11 @@ -from openpype.lib import ( +from ayon_core.lib import ( BoolDef ) -from openpype.pipeline import ( +from ayon_core.pipeline import ( load, registered_host ) -from openpype.hosts.maya.api.workfile_template_builder import ( +from ayon_core.hosts.maya.api.workfile_template_builder import ( MayaTemplateBuilder ) @@ -13,7 +13,7 @@ from openpype.hosts.maya.api.workfile_template_builder import ( class LoadAsTemplate(load.LoaderPlugin): """Load workfile as a template """ - families = ["workfile"] + product_types = {"workfile"} label = "Load as template" representations = ["ma", "mb"] icon = "wrench" From 04f57187040d5c8698b4c90cda3dbbfae050d9f5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 17:55:56 +0100 Subject: [PATCH 68/83] Bugfix: refactor `family` -> `product_type` --- client/ayon_core/pipeline/workfile/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 7faa67af04..fb357d8b9b 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -1480,7 +1480,7 @@ class PlaceholderLoadMixin(object): product_name_regex = None if product_name_regex_value: product_name_regex = re.compile(product_name_regex_value) - product_type = placeholder.data["family"] + product_type = placeholder.data["product_type"] builder_type = placeholder.data["builder_type"] folder_ids = [] From bf26eba7797d5f53a5eda6b2836c43276443d7dc Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 2 Apr 2024 12:45:57 +0200 Subject: [PATCH 69/83] Bump Houdini addon version - Color management for Houdini workfiles --- server_addon/houdini/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index b5c9b6cb71..11ef092868 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.2.12" +__version__ = "0.2.13" From eb0de8685408c95f63f8e4d9e15b1c0be926779e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 2 Apr 2024 14:43:37 +0200 Subject: [PATCH 70/83] Remove `TypedDict` to support Py 3.7 (e.g. Maya 2022) --- .../pipeline/workfile/workfile_template_builder.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index fb357d8b9b..cd63198317 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -16,7 +16,6 @@ import re import collections import copy from abc import ABCMeta, abstractmethod -from typing import TypedDict import six from ayon_api import ( @@ -53,14 +52,6 @@ from ayon_core.pipeline.create import ( _NOT_SET = object() -class TemplatePresetDict(TypedDict): - """Dictionary with `path`, `keep_placeholder` and `create_first_version` - settings from the template preset for current context.""" - path: str - keep_placeholder: bool - create_first_version: bool - - class TemplateNotFound(Exception): """Exception raised when template does not exist.""" pass @@ -782,14 +773,14 @@ class AbstractTemplateBuilder(object): - 'project_settings/{host name}/templated_workfile_build/profiles' Returns: - TemplatePresetDict: Dictionary with `path`, `keep_placeholder` and + dict: Dictionary with `path`, `keep_placeholder` and `create_first_version` settings from the template preset for current context. Raises: TemplateProfileNotFound: When profiles are not filled. TemplateLoadFailed: Profile was found but path is not set. - TemplateNotFound: Path was set but file does not exists. + TemplateNotFound: Path was set but file does not exist. """ host_name = self.host_name From ab408bd177972e1d49778fa5da1c59c7fcba04ec Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Apr 2024 16:16:06 +0200 Subject: [PATCH 71/83] Update client/ayon_core/pipeline/workfile/workfile_template_builder.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../ayon_core/pipeline/workfile/workfile_template_builder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index cd63198317..22c732a07a 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -1471,7 +1471,9 @@ class PlaceholderLoadMixin(object): product_name_regex = None if product_name_regex_value: product_name_regex = re.compile(product_name_regex_value) - product_type = placeholder.data["product_type"] + product_type = placeholder.data.get("product_type") + if product_type is None: + product_type = placeholder.data["family"] builder_type = placeholder.data["builder_type"] folder_ids = [] From 75dbba65bf02b0e3d16fc7ecb04363f940807d72 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Apr 2024 16:16:17 +0200 Subject: [PATCH 72/83] Update client/ayon_core/pipeline/workfile/workfile_template_builder.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../pipeline/workfile/workfile_template_builder.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 22c732a07a..ceac5405c5 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -498,9 +498,14 @@ class AbstractTemplateBuilder(object): process if version is created """ - if any(value is None for value in [template_path, - keep_placeholders, - create_first_version]): + if any( + value is None + for value in [ + template_path, + keep_placeholders, + create_first_version, + ] + ): template_preset = self.get_template_preset() if template_path is None: template_path = template_preset["path"] From 6f8ab66eb2684b36326a563bc7c91d792ce88484 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Apr 2024 09:32:22 +0200 Subject: [PATCH 73/83] Update client/ayon_core/hosts/maya/plugins/load/load_as_template.py Co-authored-by: Toke Jepsen --- client/ayon_core/hosts/maya/plugins/load/load_as_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_as_template.py b/client/ayon_core/hosts/maya/plugins/load/load_as_template.py index 5c546a1837..f696d369e3 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_as_template.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_as_template.py @@ -13,7 +13,7 @@ from ayon_core.hosts.maya.api.workfile_template_builder import ( class LoadAsTemplate(load.LoaderPlugin): """Load workfile as a template """ - product_types = {"workfile"} + product_types = {"workfile", "mayaScene"} label = "Load as template" representations = ["ma", "mb"] icon = "wrench" From e75f44f6c1fe200720e6af12f31a8488b825df94 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 15 Apr 2024 15:06:56 +0200 Subject: [PATCH 74/83] Bugfix: Parent to world only if not already at world - support root level placeholders --- client/ayon_core/hosts/maya/api/workfile_template_builder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/api/workfile_template_builder.py b/client/ayon_core/hosts/maya/api/workfile_template_builder.py index 75386d7e64..ddf19125e3 100644 --- a/client/ayon_core/hosts/maya/api/workfile_template_builder.py +++ b/client/ayon_core/hosts/maya/api/workfile_template_builder.py @@ -331,7 +331,8 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): if scene_parent: cmds.parent(node, scene_parent) else: - cmds.parent(node, world=True) + if cmds.listRelatives(node, parent=True): + cmds.parent(node, world=True) holding_sets = cmds.listSets(object=placeholder.scene_identifier) if not holding_sets: From a3f9651de1a8825c0827d731b1467c9f01fb3e2a Mon Sep 17 00:00:00 2001 From: Mustafa Taher Date: Tue, 16 Apr 2024 11:56:19 +0200 Subject: [PATCH 75/83] use ext.lower() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- .../hosts/houdini/plugins/publish/extract_composite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/extract_composite.py b/client/ayon_core/hosts/houdini/plugins/publish/extract_composite.py index fe88adc120..0fab69ef4a 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/extract_composite.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/extract_composite.py @@ -46,7 +46,7 @@ class ExtractComposite(publish.Extractor, "frameEnd": instance.data["frameEndHandle"], } - if ext == "exr": + if ext.lower() == "exr": # Inject colorspace with 'scene_linear' as that's the # default Houdini working colorspace and all extracted # OpenEXR images should be in that colorspace. From 00926cf9e95d19eef828344d8eff3e5c59ab532c Mon Sep 17 00:00:00 2001 From: Mustafa Taher Date: Tue, 16 Apr 2024 11:58:50 +0200 Subject: [PATCH 76/83] update import Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../hosts/houdini/hooks/set_default_display_and_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/hooks/set_default_display_and_view.py b/client/ayon_core/hosts/houdini/hooks/set_default_display_and_view.py index c9f79c74b9..2e97c06bff 100644 --- a/client/ayon_core/hosts/houdini/hooks/set_default_display_and_view.py +++ b/client/ayon_core/hosts/houdini/hooks/set_default_display_and_view.py @@ -1,4 +1,4 @@ -from ayon_core.lib.applications import PreLaunchHook, LaunchTypes +from ayon_applications import PreLaunchHook, LaunchTypes class SetDefaultDisplayView(PreLaunchHook): From b53281f4905115d18419279ef6a6846c3df95b6f Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 16 Apr 2024 12:45:34 +0200 Subject: [PATCH 77/83] update import --- client/ayon_core/hosts/houdini/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/api/lib.py b/client/ayon_core/hosts/houdini/api/lib.py index 3da81b2b51..9cac2ab210 100644 --- a/client/ayon_core/hosts/houdini/api/lib.py +++ b/client/ayon_core/hosts/houdini/api/lib.py @@ -839,7 +839,7 @@ def set_review_color_space(opengl_node, review_color_space="", log=None): # fall to default review color space if the setting is empty. if not review_color_space: - from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa + from ayon_core.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa review_color_space = get_default_display_view_colorspace() opengl_node.setParms( From e8e1a1a2dffd197e61101ccc910c9d3f69c01a1f Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 16 Apr 2024 14:09:21 +0200 Subject: [PATCH 78/83] add missing logic of set_review_color_space --- client/ayon_core/hosts/houdini/api/lib.py | 7 +- .../houdini/plugins/create/create_review.py | 43 ++++----- .../publish/validate_review_colorspace.py | 95 +++++++++++++------ 3 files changed, 86 insertions(+), 59 deletions(-) diff --git a/client/ayon_core/hosts/houdini/api/lib.py b/client/ayon_core/hosts/houdini/api/lib.py index 9cac2ab210..7ca8f7f8f0 100644 --- a/client/ayon_core/hosts/houdini/api/lib.py +++ b/client/ayon_core/hosts/houdini/api/lib.py @@ -837,16 +837,11 @@ def set_review_color_space(opengl_node, review_color_space="", log=None): " 'OpenColorIO'".format(opengl_node.path()) ) - # fall to default review color space if the setting is empty. - if not review_color_space: - from ayon_core.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa - review_color_space = get_default_display_view_colorspace() - opengl_node.setParms( {"ociocolorspace": review_color_space} ) - self.log.debug( + log.debug( "'OCIO Colorspace' parm on '{}' has been set to " "the view color space '{}'" .format(opengl_node, review_color_space) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_review.py b/client/ayon_core/hosts/houdini/plugins/create/create_review.py index 18f7ce498d..94dcf23181 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_review.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_review.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating openGL reviews.""" -from ayon_core.hosts.houdini.api import plugin +from ayon_core.hosts.houdini.api import lib, plugin from ayon_core.lib import EnumDef, BoolDef, NumberDef import os @@ -14,6 +14,13 @@ class CreateReview(plugin.HoudiniCreator): label = "Review" product_type = "review" icon = "video-camera" + review_color_space = "" + + def apply_settings(self, project_settings): + super(CreateReview, self).apply_settings(project_settings) + color_settings = project_settings["houdini"]["imageio"]["workfile"] + if color_settings["enabled"]: + self.review_color_space = color_settings.get("review_color_space") def create(self, product_name, instance_data, pre_create_data): @@ -85,10 +92,20 @@ class CreateReview(plugin.HoudiniCreator): instance_node.setParms(parms) - # Set OCIO Colorspace to the default output colorspace + # Set OCIO Colorspace to the default colorspace # if there's OCIO if os.getenv("OCIO"): - self.set_colorcorrect_to_default_view_space(instance_node) + # Fall to the default value if cls.review_color_space is empty. + if not self.review_color_space: + # cls.review_color_space is an empty string + # when the imageio/workfile setting is disabled or + # when the Review colorspace setting is empty. + from ayon_core.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa + self.review_color_space = get_default_display_view_colorspace() + + lib.set_review_color_space(instance_node, + self.review_color_space, + self.log) to_lock = ["id", "productType"] @@ -131,23 +148,3 @@ class CreateReview(plugin.HoudiniCreator): minimum=0.0001, decimals=3) ] - - def set_colorcorrect_to_default_view_space(self, - instance_node): - """Set ociocolorspace to the default output space.""" - from ayon_core.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa - - # set Color Correction parameter to OpenColorIO - instance_node.setParms({"colorcorrect": 2}) - - # Get default view space for ociocolorspace parm. - default_view_space = get_default_display_view_colorspace() - instance_node.setParms( - {"ociocolorspace": default_view_space} - ) - - self.log.debug( - "'OCIO Colorspace' parm on '{}' has been set to " - "the default view color space '{}'" - .format(instance_node, default_view_space) - ) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_review_colorspace.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_review_colorspace.py index 031138e21d..d3afa83b67 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -4,15 +4,19 @@ from ayon_core.pipeline import ( PublishValidationError, OptionalPyblishPluginMixin ) -from ayon_core.pipeline.publish import RepairAction +from ayon_core.pipeline.publish import ( + RepairAction, + get_plugin_settings, + apply_plugin_settings_automatically +) from ayon_core.hosts.houdini.api.action import SelectROPAction import os import hou -class SetDefaultViewSpaceAction(RepairAction): - label = "Set default view colorspace" +class ResetViewSpaceAction(RepairAction): + label = "Reset OCIO colorspace parm" icon = "mdi.monitor" @@ -27,9 +31,25 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, families = ["review"] hosts = ["houdini"] label = "Validate Review Colorspace" - actions = [SetDefaultViewSpaceAction, SelectROPAction] + actions = [ResetViewSpaceAction, SelectROPAction] optional = True + review_color_space = "" + + @classmethod + def apply_settings(cls, project_settings): + # Preserve automatic settings applying logic + settings = get_plugin_settings(plugin=cls, + project_settings=project_settings, + log=cls.log, + category="houdini") + apply_plugin_settings_automatically(cls, settings, logger=cls.log) + + # Add review color settings + color_settings = project_settings["houdini"]["imageio"]["workfile"] + if color_settings["enabled"]: + cls.review_color_space = color_settings.get("review_color_space") + def process(self, instance): @@ -52,39 +72,54 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, " 'OpenColorIO'".format(rop_node.path()) ) - if rop_node.evalParm("ociocolorspace") not in \ - hou.Color.ocio_spaces(): - + current_color_space = rop_node.evalParm("ociocolorspace") + if current_color_space not in hou.Color.ocio_spaces(): raise PublishValidationError( "Invalid value: Colorspace name doesn't exist.\n" "Check 'OCIO Colorspace' parameter on '{}' ROP" .format(rop_node.path()) ) - @classmethod - def repair(cls, instance): - """Set Default View Space Action. + # if houdini/imageio/workfile is enabled and + # Review colorspace setting is empty then this check should + # actually check if the current_color_space setting equals + # the default colorspace value. + # However, it will make the black cmd screen show up more often + # which is very annoying. + if self.review_color_space and \ + self.review_color_space != current_color_space: - It is a helper action more than a repair action, - used to set colorspace on opengl node to the default view. - """ - from ayon_core.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa - - rop_node = hou.node(instance.data["instance_node"]) - - if rop_node.evalParm("colorcorrect") != 2: - rop_node.setParms({"colorcorrect": 2}) - cls.log.debug( - "'Color Correction' parm on '{}' has been set to" - " 'OpenColorIO'".format(rop_node.path()) + raise PublishValidationError( + "Invalid value: Colorspace name doesn't match" + "the Colorspace specified in settings." ) - # Get default view colorspace name - default_view_space = get_default_display_view_colorspace() + @classmethod + def repair(cls, instance): + """Reset view colorspace. - rop_node.setParms({"ociocolorspace": default_view_space}) - cls.log.info( - "'OCIO Colorspace' parm on '{}' has been set to " - "the default view color space '{}'" - .format(rop_node, default_view_space) - ) + It is used to set colorspace on opengl node. + + It uses the colorspace value specified in the Houdini addon settings. + If the value in the Houdini addon settings is empty, + it will fall to the default colorspace. + + Note: + This repair action assumes that OCIO is enabled. + As if OCIO is disabled the whole validation is skipped + and this repair action won't show up. + """ + from ayon_core.hosts.houdini.api.lib import set_review_color_space + + # Fall to the default value if cls.review_color_space is empty. + if not cls.review_color_space: + # cls.review_color_space is an empty string + # when the imageio/workfile setting is disabled or + # when the Review colorspace setting is empty. + from ayon_core.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa + cls.review_color_space = get_default_display_view_colorspace() + + rop_node = hou.node(instance.data["instance_node"]) + set_review_color_space(rop_node, + cls.review_color_space, + cls.log) From 6b2b28d7d235c1da4d684e638c863305ed3b2d35 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 25 Apr 2024 14:57:58 +0800 Subject: [PATCH 79/83] make sure the collect render layer is just collecting beauty render with the global aov mode disabled in Arnold renderer --- .../ayon_core/hosts/maya/api/lib_renderproducts.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/client/ayon_core/hosts/maya/api/lib_renderproducts.py b/client/ayon_core/hosts/maya/api/lib_renderproducts.py index 832d1c21c2..b949845f1d 100644 --- a/client/ayon_core/hosts/maya/api/lib_renderproducts.py +++ b/client/ayon_core/hosts/maya/api/lib_renderproducts.py @@ -588,6 +588,20 @@ class RenderProductsArnold(ARenderProducts): "Unrecognized arnold driver format " "for AOV - {}".format(aov_name) ) + global_aov_enabled = bool( + self._get_attr( + "defaultArnoldRenderOptions.aovMode", as_string=False) + ) + if not global_aov_enabled: + for camera in cameras: + products.insert(0, + RenderProduct(productName="", + ext=ext, + driver=ai_driver, + multipart=self.multipart, + camera=camera, + colorspace=colorspace)) + return products # If aov RGBA is selected, arnold will translate it to `beauty` name = aov_name From ba1242316e228ffe98d022fc27ad81202469c3b4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 25 Apr 2024 15:12:04 +0800 Subject: [PATCH 80/83] add asstring argment in the existing aovs_enabled --- .../hosts/maya/api/lib_renderproducts.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib_renderproducts.py b/client/ayon_core/hosts/maya/api/lib_renderproducts.py index b949845f1d..52c282c6de 100644 --- a/client/ayon_core/hosts/maya/api/lib_renderproducts.py +++ b/client/ayon_core/hosts/maya/api/lib_renderproducts.py @@ -588,20 +588,6 @@ class RenderProductsArnold(ARenderProducts): "Unrecognized arnold driver format " "for AOV - {}".format(aov_name) ) - global_aov_enabled = bool( - self._get_attr( - "defaultArnoldRenderOptions.aovMode", as_string=False) - ) - if not global_aov_enabled: - for camera in cameras: - products.insert(0, - RenderProduct(productName="", - ext=ext, - driver=ai_driver, - multipart=self.multipart, - camera=camera, - colorspace=colorspace)) - return products # If aov RGBA is selected, arnold will translate it to `beauty` name = aov_name @@ -734,7 +720,8 @@ class RenderProductsArnold(ARenderProducts): # AOVs > Legacy > Maya Render View > Mode aovs_enabled = bool( - self._get_attr("defaultArnoldRenderOptions.aovMode") + self._get_attr( + "defaultArnoldRenderOptions.aovMode", as_string=False) ) if not aovs_enabled: return beauty_products From e38ef81bf21ca716f0b4c666145b7134fad9e830 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 25 Apr 2024 11:00:36 +0200 Subject: [PATCH 81/83] add WorkfileImageIO settings --- server_addon/houdini/package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/package.py b/server_addon/houdini/package.py index 4b72af2a89..4e441c76ae 100644 --- a/server_addon/houdini/package.py +++ b/server_addon/houdini/package.py @@ -1,3 +1,3 @@ name = "houdini" title = "Houdini" -version = "0.2.12" +version = "0.2.13" From 129070aefec7a6b855f80b95933845298fb2465e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 25 Apr 2024 17:53:29 +0800 Subject: [PATCH 82/83] make sure dict for Alembic Extractors having the attributes for enabling it --- .../hosts/max/plugins/publish/extract_alembic.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/max/plugins/publish/extract_alembic.py b/client/ayon_core/hosts/max/plugins/publish/extract_alembic.py index 67b5174200..67cec23ecc 100644 --- a/client/ayon_core/hosts/max/plugins/publish/extract_alembic.py +++ b/client/ayon_core/hosts/max/plugins/publish/extract_alembic.py @@ -53,6 +53,7 @@ class ExtractAlembic(publish.Extractor, hosts = ["max"] families = ["pointcache"] optional = True + active = True def process(self, instance): if not self.is_active(instance.data): @@ -102,24 +103,27 @@ class ExtractAlembic(publish.Extractor, @classmethod def get_attribute_defs(cls): - return [ + defs = super(ExtractAlembic, cls).get_attribute_defs() + defs.extend([ BoolDef("custom_attrs", label="Custom Attributes", default=False), - ] + ]) + return defs class ExtractCameraAlembic(ExtractAlembic): """Extract Camera with AlembicExport.""" - label = "Extract Alembic Camera" families = ["camera"] + optional = True -class ExtractModel(ExtractAlembic): +class ExtractModelAlembic(ExtractAlembic): """Extract Geometry in Alembic Format""" label = "Extract Geometry (Alembic)" families = ["model"] + optional = True def _set_abc_attributes(self, instance): attr_values = self.get_attr_values_from_data(instance.data) From 8c2bc797e654873e50964e25a23bb2a1b85b22d2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:38:57 +0200 Subject: [PATCH 83/83] fix import in create ayon addons --- server_addon/create_ayon_addons.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/create_ayon_addons.py b/server_addon/create_ayon_addons.py index 79b9aa5450..f0a36d4740 100644 --- a/server_addon/create_ayon_addons.py +++ b/server_addon/create_ayon_addons.py @@ -5,7 +5,7 @@ import shutil import argparse import zipfile import types -import importlib +import importlib.machinery import platform import collections from pathlib import Path