From 4f4a695567a985f3b032ca7fe50ee4db6159fa9a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 1 Jul 2024 15:27:49 +0200 Subject: [PATCH 01/14] AY-5714 - added default deadline username and password to Settings These values should be used if studio has only single credentials for communicating with Deadline webservice. --- server_addon/deadline/server/settings/main.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/server_addon/deadline/server/settings/main.py b/server_addon/deadline/server/settings/main.py index 47ad72a86f..53d2234ac7 100644 --- a/server_addon/deadline/server/settings/main.py +++ b/server_addon/deadline/server/settings/main.py @@ -6,6 +6,7 @@ from ayon_server.settings import ( SettingsField, ensure_unique_names, ) +from ayon_server.settings.enum import secrets_enum if TYPE_CHECKING: from ayon_server.addons import BaseServerAddon @@ -34,13 +35,26 @@ async def defined_deadline_ws_name_enum_resolver( class ServerItemSubmodel(BaseSettingsModel): """Connection info about configured DL servers.""" - _layout = "compact" + _layout = "expanded" name: str = SettingsField(title="Name") value: str = SettingsField(title="Url") require_authentication: bool = SettingsField( False, title="Require authentication") not_verify_ssl: bool = SettingsField( False, title="Don't verify SSL") + default_username: str = SettingsField( + title="Default user name", + description="Webservice username, 'Require authentication' must be " + "enabled." + ) + default_password: str = SettingsField( + "", + placeholder="Select password from Ayon secrets", + enum_resolver=secrets_enum, + title="Default password", + description="Webservice password, 'Require authentication' must be " + "enabled." + ) class DeadlineSettings(BaseSettingsModel): @@ -77,7 +91,10 @@ DEFAULT_VALUES = { "name": "default", "value": "http://127.0.0.1:8082", "require_authentication": False, - "not_verify_ssl": False + "not_verify_ssl": False, + "default_username": "", + "default_password": "" + } ], "deadline_server": "default", From e9f1e7475709ed0521eb57bf4168c3558adc50da Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 1 Jul 2024 15:28:28 +0200 Subject: [PATCH 02/14] AY-5714 - collect default deadline username and password from Settings These values should be used if studio has only single credentials for communicating with Deadline webservice. Could be overridden by values from Site Settings. --- .../publish/collect_user_credentials.py | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_user_credentials.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_user_credentials.py index ab96ba5828..765f018846 100644 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_user_credentials.py +++ b/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_user_credentials.py @@ -12,13 +12,18 @@ Provides: """ import pyblish.api -from ayon_api import get_server_api_connection +from ayon_api import get_server_api_connection, get_secret +from ayon_core.pipeline import KnownPublishError from ayon_deadline.lib import FARM_FAMILIES class CollectDeadlineUserCredentials(pyblish.api.InstancePlugin): """Collects user name and password for artist if DL requires authentication + + If Deadline server is marked to require authentication, it looks first for + default values in 'Studio Settings', which could be overriden by artist + dependent values from 'Site settings`. """ order = pyblish.api.CollectorOrder + 0.250 label = "Collect Deadline User Credentials" @@ -72,6 +77,18 @@ class CollectDeadlineUserCredentials(pyblish.api.InstancePlugin): addons_manager = instance.context.data["ayonAddonsManager"] deadline_addon = addons_manager["deadline"] + + default_username = deadline_info["default_username"] + secret_name = deadline_info["default_password"] + secret = get_secret(secret_name) + if not secret: + raise KnownPublishError(f"'{secret_name}' secret not found") + default_password = secret["value"] + if default_username and default_password: + self.log.debug("Setting credentials from defaults") + instance.data["deadline"]["auth"] = (default_username, + default_password) + # TODO import 'get_addon_site_settings' when available # in public 'ayon_api' local_settings = get_server_api_connection().get_addon_site_settings( @@ -79,5 +96,8 @@ class CollectDeadlineUserCredentials(pyblish.api.InstancePlugin): local_settings = local_settings["local_settings"] for server_info in local_settings: if deadline_server_name == server_info["server_name"]: - instance.data["deadline"]["auth"] = (server_info["username"], - server_info["password"]) + if server_info["username"] and server_info["password"]: + self.log.debug("Setting credentials from Site Settings") + instance.data["deadline"]["auth"] = \ + (server_info["username"], server_info["password"]) + break From 5b1c785eacdecdc766f7bbdafbe26dae9a7e5131 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 2 Jul 2024 13:52:30 +0200 Subject: [PATCH 03/14] Bump version of deadline Added default user credentials --- server_addon/deadline/package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/deadline/package.py b/server_addon/deadline/package.py index dcc61e3d46..8fcc007850 100644 --- a/server_addon/deadline/package.py +++ b/server_addon/deadline/package.py @@ -1,6 +1,6 @@ name = "deadline" title = "Deadline" -version = "0.2.2" +version = "0.2.3" client_dir = "ayon_deadline" From 550cb51169a781ce21c3378741ca9de1059481e3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 2 Jul 2024 14:20:52 +0200 Subject: [PATCH 04/14] Fix default value for default_username --- server_addon/deadline/server/settings/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server_addon/deadline/server/settings/main.py b/server_addon/deadline/server/settings/main.py index 53d2234ac7..dcd7cd75dd 100644 --- a/server_addon/deadline/server/settings/main.py +++ b/server_addon/deadline/server/settings/main.py @@ -43,6 +43,7 @@ class ServerItemSubmodel(BaseSettingsModel): not_verify_ssl: bool = SettingsField( False, title="Don't verify SSL") default_username: str = SettingsField( + "", title="Default user name", description="Webservice username, 'Require authentication' must be " "enabled." From 00f5bd022678fca4dde9db6a8cb464e9f2ad9dc2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 2 Jul 2024 14:21:59 +0200 Subject: [PATCH 05/14] Bump even client version --- server_addon/deadline/client/ayon_deadline/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/deadline/client/ayon_deadline/version.py b/server_addon/deadline/client/ayon_deadline/version.py index e131427f12..96262d7186 100644 --- a/server_addon/deadline/client/ayon_deadline/version.py +++ b/server_addon/deadline/client/ayon_deadline/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'deadline' version.""" -__version__ = "0.2.2" +__version__ = "0.2.3" From 1ab079778fad343949ac45ecbc5722e50e964112 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 2 Jul 2024 17:59:31 +0200 Subject: [PATCH 06/14] Reverted use of secrets for deadline password Regular artists don't have access to AYON secret, therefore it cannot be used. Encrypted or at least crossed field for passwords doesn't exist so far. --- .../plugins/publish/collect_user_credentials.py | 9 ++------- server_addon/deadline/server/settings/main.py | 3 --- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_user_credentials.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_user_credentials.py index 765f018846..1c59c178d3 100644 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_user_credentials.py +++ b/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_user_credentials.py @@ -12,8 +12,7 @@ Provides: """ import pyblish.api -from ayon_api import get_server_api_connection, get_secret -from ayon_core.pipeline import KnownPublishError +from ayon_api import get_server_api_connection from ayon_deadline.lib import FARM_FAMILIES @@ -79,11 +78,7 @@ class CollectDeadlineUserCredentials(pyblish.api.InstancePlugin): deadline_addon = addons_manager["deadline"] default_username = deadline_info["default_username"] - secret_name = deadline_info["default_password"] - secret = get_secret(secret_name) - if not secret: - raise KnownPublishError(f"'{secret_name}' secret not found") - default_password = secret["value"] + default_password = deadline_info["default_password"] if default_username and default_password: self.log.debug("Setting credentials from defaults") instance.data["deadline"]["auth"] = (default_username, diff --git a/server_addon/deadline/server/settings/main.py b/server_addon/deadline/server/settings/main.py index dcd7cd75dd..edb8a16e35 100644 --- a/server_addon/deadline/server/settings/main.py +++ b/server_addon/deadline/server/settings/main.py @@ -6,7 +6,6 @@ from ayon_server.settings import ( SettingsField, ensure_unique_names, ) -from ayon_server.settings.enum import secrets_enum if TYPE_CHECKING: from ayon_server.addons import BaseServerAddon @@ -50,8 +49,6 @@ class ServerItemSubmodel(BaseSettingsModel): ) default_password: str = SettingsField( "", - placeholder="Select password from Ayon secrets", - enum_resolver=secrets_enum, title="Default password", description="Webservice password, 'Require authentication' must be " "enabled." From 6467df96f45b434227b674d96cad810cebb878f6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 4 Jul 2024 13:43:53 +0200 Subject: [PATCH 07/14] fill task short name using project entity --- .../ayon_core/pipeline/create/product_name.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index fecda867e5..6f8a43cdbe 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -1,3 +1,5 @@ +import ayon_api + from ayon_core.settings import get_project_settings from ayon_core.lib import filter_profiles, prepare_template_data @@ -88,6 +90,7 @@ def get_product_name( dynamic_data=None, project_settings=None, product_type_filter=None, + project_entity=None, ): """Calculate product name based on passed context and AYON settings. @@ -120,6 +123,8 @@ def get_product_name( product_type_filter (Optional[str]): Use different product type for product template filtering. Value of `product_type` is used when not passed. + project_entity (Optional[Dict[str, Any]]): Project entity used when + task short name is required by template. Raises: TemplateFillError: If filled template contains placeholder key which @@ -150,6 +155,18 @@ def get_product_name( if "{task}" in template.lower(): task_value = task_name + elif "{task[short]}" in template.lower(): + # NOTE this is very inefficient approach + # - project entity should be required + if project_entity is None: + project_entity = ayon_api.get_project(project_name) + task_types_by_name = { + task["name"]: task for task in + project_entity["taskTypes"] + } + task_short = task_types_by_name.get(task_type, {}).get("shortName") + task_value["short"] = task_short + fill_pairs = { "variant": variant, "family": product_type, From dc43d25d31d966ada276666387afc4329be34b0c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jul 2024 14:11:15 +0200 Subject: [PATCH 08/14] remove deprecated '-intra' --- client/ayon_core/lib/transcoding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 4d778c2091..bff28614ea 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -978,7 +978,7 @@ def _ffmpeg_h264_codec_args(stream_data, source_ffmpeg_cmd): if pix_fmt: output.extend(["-pix_fmt", pix_fmt]) - output.extend(["-intra", "-g", "1"]) + output.extend(["-g", "1"]) return output From 3dffcaff01534c3eceaf8306c6398a3d70cb85cf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:34:36 +0200 Subject: [PATCH 09/14] create context does pass in project entity --- client/ayon_core/pipeline/create/context.py | 29 ++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 0d8722dab1..066a147479 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -13,6 +13,7 @@ import pyblish.api import ayon_api from ayon_core.settings import get_project_settings +from ayon_core.lib import is_func_signature_supported from ayon_core.lib.attribute_definitions import ( UnknownDef, serialize_attr_defs, @@ -1404,6 +1405,7 @@ class CreateContext: self._current_workfile_path = None self._current_project_settings = None + self._current_project_entity = _NOT_SET self._current_folder_entity = _NOT_SET self._current_task_entity = _NOT_SET self._current_task_type = _NOT_SET @@ -1592,6 +1594,22 @@ class CreateContext: self._current_task_type = task_type return self._current_task_type + def get_current_project_entity(self): + """Project entity for current context project. + + Returns: + Union[dict[str, Any], None]: Folder entity. + + """ + if self._current_project_entity is not _NOT_SET: + return copy.deepcopy(self._current_project_entity) + project_entity = None + project_name = self.get_current_project_name() + if project_name: + project_entity = ayon_api.get_project(project_name) + self._current_project_entity = project_entity + return copy.deepcopy(self._current_project_entity) + def get_current_folder_entity(self): """Folder entity for current context folder. @@ -1788,6 +1806,7 @@ class CreateContext: self._current_task_name = task_name self._current_workfile_path = workfile_path + self._current_project_entity = _NOT_SET self._current_folder_entity = _NOT_SET self._current_task_entity = _NOT_SET self._current_task_type = _NOT_SET @@ -2083,13 +2102,21 @@ class CreateContext: # TODO validate types _pre_create_data.update(pre_create_data) - product_name = creator.get_product_name( + project_entity = self.get_current_project_entity() + args = ( project_name, folder_entity, task_entity, variant, self.host_name, ) + kwargs = {"project_entity": project_entity} + # Backwards compatibility for 'project_entity' argument + if not is_func_signature_supported( + creator.get_product_name, *args, **kwargs + ): + kwargs.pop("project_entity") + product_name = creator.get_product_name(*args, **kwargs) instance_data = { "folderPath": folder_entity["path"], From 2f0a6847ab4d72cf65329b0bcac5234d35db519d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:43:44 +0200 Subject: [PATCH 10/14] prefill project entity where we know it may be used --- client/ayon_core/pipeline/create/context.py | 1 + .../pipeline/create/creator_plugins.py | 10 ++++++---- .../ayon_core/pipeline/create/product_name.py | 8 +++++--- client/ayon_core/tools/publisher/abstract.py | 6 ++++++ client/ayon_core/tools/publisher/control.py | 3 +++ .../tools/publisher/models/create.py | 19 ++++++++++++++++--- 6 files changed, 37 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 066a147479..f97d34d305 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -2112,6 +2112,7 @@ class CreateContext: ) kwargs = {"project_entity": project_entity} # Backwards compatibility for 'project_entity' argument + # - 'get_product_name' signature changed 24/07/08 if not is_func_signature_supported( creator.get_product_name, *args, **kwargs ): diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index e0b30763d0..8cacf7a1d0 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -297,7 +297,6 @@ class BaseCreator: )) setattr(self, key, value) - @property def identifier(self): """Identifier of creator (must be unique). @@ -493,7 +492,8 @@ class BaseCreator: task_entity, variant, host_name=None, - instance=None + instance=None, + project_entity=None, ): """Return product name for passed context. @@ -510,8 +510,9 @@ class BaseCreator: instance (Optional[CreatedInstance]): Object of 'CreatedInstance' for which is product name updated. Passed only on product name update. - """ + project_entity (Optional[dict[str, Any]]): Project entity. + """ if host_name is None: host_name = self.create_context.host_name @@ -537,7 +538,8 @@ class BaseCreator: self.product_type, variant, dynamic_data=dynamic_data, - project_settings=self.project_settings + project_settings=self.project_settings, + project_entity=project_entity, ) def get_instance_attr_defs(self): diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 6f8a43cdbe..cd28a6eef0 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -126,11 +126,15 @@ def get_product_name( project_entity (Optional[Dict[str, Any]]): Project entity used when task short name is required by template. + Returns: + str: Product name. + Raises: + TaskNotSetError: If template requires task which is not provided. TemplateFillError: If filled template contains placeholder key which is not collected. - """ + """ if not product_type: return "" @@ -156,8 +160,6 @@ def get_product_name( task_value = task_name elif "{task[short]}" in template.lower(): - # NOTE this is very inefficient approach - # - project entity should be required if project_entity is None: project_entity = ayon_api.get_project(project_name) task_types_by_name = { diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index a9142396f5..768f4b052f 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -166,6 +166,12 @@ class AbstractPublisherBackend(AbstractPublisherCommon): ) -> Union[TaskItem, None]: pass + @abstractmethod + def get_project_entity( + self, project_name: str + ) -> Union[Dict[str, Any], None]: + pass + @abstractmethod def get_folder_entity( self, project_name: str, folder_id: str diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index f26f8fc524..257b45de08 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -193,6 +193,9 @@ class PublisherController( def get_convertor_items(self): return self._create_model.get_convertor_items() + def get_project_entity(self, project_name): + return self._projects_model.get_project_entity(project_name) + def get_folder_type_items(self, project_name, sender=None): return self._projects_model.get_folder_type_items( project_name, sender diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 6da3a51a31..ab2bf07614 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -9,6 +9,7 @@ from ayon_core.lib.attribute_definitions import ( ) from ayon_core.lib.profiles_filtering import filter_profiles from ayon_core.lib.attribute_definitions import UIDef +from ayon_core.lib import is_func_signature_supported from ayon_core.pipeline.create import ( BaseCreator, AutoCreator, @@ -26,6 +27,7 @@ from ayon_core.tools.publisher.abstract import ( AbstractPublisherBackend, CardMessageTypes, ) + CREATE_EVENT_SOURCE = "publisher.create.model" @@ -356,13 +358,24 @@ class CreateModel: project_name, task_item.task_id ) - return creator.get_product_name( + project_entity = self._controller.get_project_entity(project_name) + args = ( project_name, folder_entity, task_entity, - variant, - instance=instance + variant ) + kwargs = { + "instance": instance, + "project_entity": project_entity, + } + # Backwards compatibility for 'project_entity' argument + # - 'get_product_name' signature changed 24/07/08 + if not is_func_signature_supported( + creator.get_product_name, *args, **kwargs + ): + kwargs.pop("project_entity") + return creator.get_product_name(*args, **kwargs) def create( self, From 2beac025f72a55796d7880ec8deec9e8fd09f9d9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:59:35 +0200 Subject: [PATCH 11/14] removed deadline addon --- .../deadline/client/ayon_deadline/__init__.py | 8 - .../ayon_deadline/abstract_submit_deadline.py | 617 ------------ .../deadline/client/ayon_deadline/addon.py | 81 -- .../deadline/client/ayon_deadline/lib.py | 10 - .../collect_deadline_server_from_instance.py | 115 --- .../collect_default_deadline_server.py | 48 - .../plugins/publish/collect_pools.py | 91 -- .../publish/collect_user_credentials.py | 98 -- .../help/validate_deadline_connection.xml | 17 - .../publish/help/validate_deadline_pools.xml | 31 - .../publish/submit_aftereffects_deadline.py | 143 --- .../publish/submit_blender_deadline.py | 225 ----- .../publish/submit_celaction_deadline.py | 271 ----- .../plugins/publish/submit_fusion_deadline.py | 253 ----- .../publish/submit_harmony_deadline.py | 420 -------- .../publish/submit_houdini_cache_deadline.py | 181 ---- .../publish/submit_houdini_render_deadline.py | 403 -------- .../plugins/publish/submit_max_deadline.py | 431 -------- .../plugins/publish/submit_maya_deadline.py | 935 ------------------ .../plugins/publish/submit_nuke_deadline.py | 558 ----------- .../publish/submit_publish_cache_job.py | 463 --------- .../plugins/publish/submit_publish_job.py | 585 ----------- .../publish/validate_deadline_connection.py | 52 - .../publish/validate_deadline_pools.py | 84 -- .../validate_expected_and_rendered_files.py | 256 ----- .../repository/custom/plugins/Ayon/Ayon.ico | Bin 7679 -> 0 bytes .../custom/plugins/Ayon/Ayon.options | 9 - .../repository/custom/plugins/Ayon/Ayon.param | 35 - .../repository/custom/plugins/Ayon/Ayon.py | 159 --- .../custom/plugins/CelAction/CelAction.ico | Bin 103192 -> 0 bytes .../custom/plugins/CelAction/CelAction.param | 38 - .../custom/plugins/CelAction/CelAction.py | 122 --- .../custom/plugins/GlobalJobPreLoad.py | 662 ------------- .../plugins/HarmonyAYON/HarmonyAYON.ico | Bin 1150 -> 0 bytes .../plugins/HarmonyAYON/HarmonyAYON.options | 532 ---------- .../plugins/HarmonyAYON/HarmonyAYON.param | 98 -- .../custom/plugins/HarmonyAYON/HarmonyAYON.py | 151 --- .../OpenPypeTileAssembler.ico | Bin 126987 -> 0 bytes .../OpenPypeTileAssembler.options | 35 - .../OpenPypeTileAssembler.param | 17 - .../OpenPypeTileAssembler.py | 457 --------- .../client/ayon_deadline/repository/readme.md | 29 - .../deadline/client/ayon_deadline/version.py | 3 - server_addon/deadline/package.py | 10 - server_addon/deadline/server/__init__.py | 15 - .../deadline/server/settings/__init__.py | 12 - server_addon/deadline/server/settings/main.py | 100 -- .../server/settings/publish_plugins.py | 578 ----------- .../deadline/server/settings/site_settings.py | 28 - 49 files changed, 9466 deletions(-) delete mode 100644 server_addon/deadline/client/ayon_deadline/__init__.py delete mode 100644 server_addon/deadline/client/ayon_deadline/abstract_submit_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/addon.py delete mode 100644 server_addon/deadline/client/ayon_deadline/lib.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/collect_deadline_server_from_instance.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/collect_default_deadline_server.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/collect_pools.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/collect_user_credentials.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/help/validate_deadline_connection.xml delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/help/validate_deadline_pools.xml delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_aftereffects_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_blender_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_celaction_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_fusion_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_harmony_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_houdini_cache_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_houdini_render_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_max_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_maya_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_nuke_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_publish_cache_job.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_publish_job.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/validate_deadline_connection.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/validate_deadline_pools.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/validate_expected_and_rendered_files.py delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/Ayon/Ayon.ico delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/Ayon/Ayon.options delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/Ayon/Ayon.param delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/Ayon/Ayon.py delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.ico delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.param delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.py delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/GlobalJobPreLoad.py delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.ico delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.options delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.param delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.py delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.ico delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.options delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.param delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/readme.md delete mode 100644 server_addon/deadline/client/ayon_deadline/version.py delete mode 100644 server_addon/deadline/package.py delete mode 100644 server_addon/deadline/server/__init__.py delete mode 100644 server_addon/deadline/server/settings/__init__.py delete mode 100644 server_addon/deadline/server/settings/main.py delete mode 100644 server_addon/deadline/server/settings/publish_plugins.py delete mode 100644 server_addon/deadline/server/settings/site_settings.py diff --git a/server_addon/deadline/client/ayon_deadline/__init__.py b/server_addon/deadline/client/ayon_deadline/__init__.py deleted file mode 100644 index 6fec1006e6..0000000000 --- a/server_addon/deadline/client/ayon_deadline/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .addon import DeadlineAddon -from .version import __version__ - - -__all__ = ( - "DeadlineAddon", - "__version__" -) diff --git a/server_addon/deadline/client/ayon_deadline/abstract_submit_deadline.py b/server_addon/deadline/client/ayon_deadline/abstract_submit_deadline.py deleted file mode 100644 index ba50aaccf7..0000000000 --- a/server_addon/deadline/client/ayon_deadline/abstract_submit_deadline.py +++ /dev/null @@ -1,617 +0,0 @@ -# -*- coding: utf-8 -*- -"""Abstract package for submitting jobs to Deadline. - -It provides Deadline JobInfo data class. - -""" -import json.decoder -import os -from abc import abstractmethod -import platform -import getpass -from functools import partial -from collections import OrderedDict - -import six -import attr -import requests - -import pyblish.api -from ayon_core.pipeline.publish import ( - AbstractMetaInstancePlugin, - KnownPublishError, - AYONPyblishPluginMixin -) -from ayon_core.pipeline.publish.lib import ( - replace_with_published_scene_path -) - -JSONDecodeError = getattr(json.decoder, "JSONDecodeError", ValueError) - - -def requests_post(*args, **kwargs): - """Wrap request post method. - - Disabling SSL certificate validation if ``verify`` kwarg is set to False. - This is useful when Deadline server is - running with self-signed certificates and its certificate is not - added to trusted certificates on client machines. - - Warning: - Disabling SSL certificate validation is defeating one line - of defense SSL is providing, and it is not recommended. - - """ - auth = kwargs.get("auth") - if auth: - kwargs["auth"] = tuple(auth) # explicit cast to tuple - # add 10sec timeout before bailing out - kwargs['timeout'] = 10 - return requests.post(*args, **kwargs) - - -def requests_get(*args, **kwargs): - """Wrap request get method. - - Disabling SSL certificate validation if ``verify`` kwarg is set to False. - This is useful when Deadline server is - running with self-signed certificates and its certificate is not - added to trusted certificates on client machines. - - Warning: - Disabling SSL certificate validation is defeating one line - of defense SSL is providing, and it is not recommended. - - """ - auth = kwargs.get("auth") - if auth: - kwargs["auth"] = tuple(auth) - # add 10sec timeout before bailing out - kwargs['timeout'] = 10 - return requests.get(*args, **kwargs) - - -class DeadlineKeyValueVar(dict): - """ - - Serializes dictionary key values as "{key}={value}" like Deadline uses - for EnvironmentKeyValue. - - As an example: - EnvironmentKeyValue0="A_KEY=VALUE_A" - EnvironmentKeyValue1="OTHER_KEY=VALUE_B" - - The keys are serialized in alphabetical order (sorted). - - Example: - >>> var = DeadlineKeyValueVar("EnvironmentKeyValue") - >>> var["my_var"] = "hello" - >>> var["my_other_var"] = "hello2" - >>> var.serialize() - - - """ - def __init__(self, key): - super(DeadlineKeyValueVar, self).__init__() - self.__key = key - - def serialize(self): - key = self.__key - - # Allow custom location for index in serialized string - if "{}" not in key: - key = key + "{}" - - return { - key.format(index): "{}={}".format(var_key, var_value) - for index, (var_key, var_value) in enumerate(sorted(self.items())) - } - - -class DeadlineIndexedVar(dict): - """ - - Allows to set and query values by integer indices: - Query: var[1] or var.get(1) - Set: var[1] = "my_value" - Append: var += "value" - - Note: Iterating the instance is not guarantueed to be the order of the - indices. To do so iterate with `sorted()` - - """ - def __init__(self, key): - super(DeadlineIndexedVar, self).__init__() - self.__key = key - - def serialize(self): - key = self.__key - - # Allow custom location for index in serialized string - if "{}" not in key: - key = key + "{}" - - return { - key.format(index): value for index, value in sorted(self.items()) - } - - def next_available_index(self): - # Add as first unused entry - i = 0 - while i in self.keys(): - i += 1 - return i - - def update(self, data): - # Force the integer key check - for key, value in data.items(): - self.__setitem__(key, value) - - def __iadd__(self, other): - index = self.next_available_index() - self[index] = other - return self - - def __setitem__(self, key, value): - if not isinstance(key, int): - raise TypeError("Key must be an integer: {}".format(key)) - - if key < 0: - raise ValueError("Negative index can't be set: {}".format(key)) - dict.__setitem__(self, key, value) - - -@attr.s -class DeadlineJobInfo(object): - """Mapping of all Deadline *JobInfo* attributes. - - This contains all JobInfo attributes plus their default values. - Those attributes set to `None` shouldn't be posted to Deadline as - the only required one is `Plugin`. Their default values used by Deadline - are stated in - comments. - - ..seealso: - https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/manual-submission.html - - """ - - # Required - # ---------------------------------------------- - Plugin = attr.ib() - - # General - Frames = attr.ib(default=None) # default: 0 - Name = attr.ib(default="Untitled") - Comment = attr.ib(default=None) # default: empty - Department = attr.ib(default=None) # default: empty - BatchName = attr.ib(default=None) # default: empty - UserName = attr.ib(default=getpass.getuser()) - MachineName = attr.ib(default=platform.node()) - Pool = attr.ib(default=None) # default: "none" - SecondaryPool = attr.ib(default=None) - Group = attr.ib(default=None) # default: "none" - Priority = attr.ib(default=50) - ChunkSize = attr.ib(default=1) - ConcurrentTasks = attr.ib(default=1) - LimitConcurrentTasksToNumberOfCpus = attr.ib( - default=None) # default: "true" - OnJobComplete = attr.ib(default="Nothing") - SynchronizeAllAuxiliaryFiles = attr.ib(default=None) # default: false - ForceReloadPlugin = attr.ib(default=None) # default: false - Sequential = attr.ib(default=None) # default: false - SuppressEvents = attr.ib(default=None) # default: false - Protected = attr.ib(default=None) # default: false - InitialStatus = attr.ib(default="Active") - NetworkRoot = attr.ib(default=None) - - # Timeouts - # ---------------------------------------------- - MinRenderTimeSeconds = attr.ib(default=None) # Default: 0 - MinRenderTimeMinutes = attr.ib(default=None) # Default: 0 - TaskTimeoutSeconds = attr.ib(default=None) # Default: 0 - TaskTimeoutMinutes = attr.ib(default=None) # Default: 0 - StartJobTimeoutSeconds = attr.ib(default=None) # Default: 0 - StartJobTimeoutMinutes = attr.ib(default=None) # Default: 0 - InitializePluginTimeoutSeconds = attr.ib(default=None) # Default: 0 - # can be one of - OnTaskTimeout = attr.ib(default=None) # Default: Error - EnableTimeoutsForScriptTasks = attr.ib(default=None) # Default: false - EnableFrameTimeouts = attr.ib(default=None) # Default: false - EnableAutoTimeout = attr.ib(default=None) # Default: false - - # Interruptible - # ---------------------------------------------- - Interruptible = attr.ib(default=None) # Default: false - InterruptiblePercentage = attr.ib(default=None) - RemTimeThreshold = attr.ib(default=None) - - # Notifications - # ---------------------------------------------- - # can be comma separated list of users - NotificationTargets = attr.ib(default=None) # Default: blank - ClearNotificationTargets = attr.ib(default=None) # Default: false - # A comma separated list of additional email addresses - NotificationEmails = attr.ib(default=None) # Default: blank - OverrideNotificationMethod = attr.ib(default=None) # Default: false - EmailNotification = attr.ib(default=None) # Default: false - PopupNotification = attr.ib(default=None) # Default: false - # String with `[EOL]` used for end of line - NotificationNote = attr.ib(default=None) # Default: blank - - # Machine Limit - # ---------------------------------------------- - MachineLimit = attr.ib(default=None) # Default: 0 - MachineLimitProgress = attr.ib(default=None) # Default: -1.0 - Whitelist = attr.ib(default=None) # Default: blank - Blacklist = attr.ib(default=None) # Default: blank - - # Limits - # ---------------------------------------------- - # comma separated list of limit groups - LimitGroups = attr.ib(default=None) # Default: blank - - # Dependencies - # ---------------------------------------------- - # comma separated list of job IDs - JobDependencies = attr.ib(default=None) # Default: blank - JobDependencyPercentage = attr.ib(default=None) # Default: -1 - IsFrameDependent = attr.ib(default=None) # Default: false - FrameDependencyOffsetStart = attr.ib(default=None) # Default: 0 - FrameDependencyOffsetEnd = attr.ib(default=None) # Default: 0 - ResumeOnCompleteDependencies = attr.ib(default=None) # Default: true - ResumeOnDeletedDependencies = attr.ib(default=None) # Default: false - ResumeOnFailedDependencies = attr.ib(default=None) # Default: false - # comma separated list of asset paths - RequiredAssets = attr.ib(default=None) # Default: blank - # comma separated list of script paths - ScriptDependencies = attr.ib(default=None) # Default: blank - - # Failure Detection - # ---------------------------------------------- - OverrideJobFailureDetection = attr.ib(default=None) # Default: false - FailureDetectionJobErrors = attr.ib(default=None) # 0..x - OverrideTaskFailureDetection = attr.ib(default=None) # Default: false - FailureDetectionTaskErrors = attr.ib(default=None) # 0..x - IgnoreBadJobDetection = attr.ib(default=None) # Default: false - SendJobErrorWarning = attr.ib(default=None) # Default: false - - # Cleanup - # ---------------------------------------------- - DeleteOnComplete = attr.ib(default=None) # Default: false - ArchiveOnComplete = attr.ib(default=None) # Default: false - OverrideAutoJobCleanup = attr.ib(default=None) # Default: false - OverrideJobCleanup = attr.ib(default=None) - JobCleanupDays = attr.ib(default=None) # Default: false - # - OverrideJobCleanupType = attr.ib(default=None) - - # Scheduling - # ---------------------------------------------- - # - ScheduledType = attr.ib(default=None) # Default: None - #
- ScheduledStartDateTime = attr.ib(default=None) - ScheduledDays = attr.ib(default=None) # Default: 1 - # - JobDelay = attr.ib(default=None) - # Time= - Scheduled = attr.ib(default=None) - - # Scripts - # ---------------------------------------------- - # all accept path to script - PreJobScript = attr.ib(default=None) # Default: blank - PostJobScript = attr.ib(default=None) # Default: blank - PreTaskScript = attr.ib(default=None) # Default: blank - PostTaskScript = attr.ib(default=None) # Default: blank - - # Event Opt-Ins - # ---------------------------------------------- - # comma separated list of plugins - EventOptIns = attr.ib(default=None) # Default: blank - - # Environment - # ---------------------------------------------- - EnvironmentKeyValue = attr.ib(factory=partial(DeadlineKeyValueVar, - "EnvironmentKeyValue")) - - IncludeEnvironment = attr.ib(default=None) # Default: false - UseJobEnvironmentOnly = attr.ib(default=None) # Default: false - CustomPluginDirectory = attr.ib(default=None) # Default: blank - - # Job Extra Info - # ---------------------------------------------- - ExtraInfo = attr.ib(factory=partial(DeadlineIndexedVar, "ExtraInfo")) - ExtraInfoKeyValue = attr.ib(factory=partial(DeadlineKeyValueVar, - "ExtraInfoKeyValue")) - - # Task Extra Info Names - # ---------------------------------------------- - OverrideTaskExtraInfoNames = attr.ib(default=None) # Default: false - TaskExtraInfoName = attr.ib(factory=partial(DeadlineIndexedVar, - "TaskExtraInfoName")) - - # Output - # ---------------------------------------------- - OutputFilename = attr.ib(factory=partial(DeadlineIndexedVar, - "OutputFilename")) - OutputFilenameTile = attr.ib(factory=partial(DeadlineIndexedVar, - "OutputFilename{}Tile")) - OutputDirectory = attr.ib(factory=partial(DeadlineIndexedVar, - "OutputDirectory")) - - # Asset Dependency - # ---------------------------------------------- - AssetDependency = attr.ib(factory=partial(DeadlineIndexedVar, - "AssetDependency")) - - # Tile Job - # ---------------------------------------------- - TileJob = attr.ib(default=None) # Default: false - TileJobFrame = attr.ib(default=None) # Default: 0 - TileJobTilesInX = attr.ib(default=None) # Default: 0 - TileJobTilesInY = attr.ib(default=None) # Default: 0 - TileJobTileCount = attr.ib(default=None) # Default: 0 - - # Maintenance Job - # ---------------------------------------------- - MaintenanceJob = attr.ib(default=None) # Default: false - MaintenanceJobStartFrame = attr.ib(default=None) # Default: 0 - MaintenanceJobEndFrame = attr.ib(default=None) # Default: 0 - - def serialize(self): - """Return all data serialized as dictionary. - - Returns: - OrderedDict: all serialized data. - - """ - def filter_data(a, v): - if isinstance(v, (DeadlineIndexedVar, DeadlineKeyValueVar)): - return False - if v is None: - return False - return True - - serialized = attr.asdict( - self, dict_factory=OrderedDict, filter=filter_data) - - # Custom serialize these attributes - for attribute in [ - self.EnvironmentKeyValue, - self.ExtraInfo, - self.ExtraInfoKeyValue, - self.TaskExtraInfoName, - self.OutputFilename, - self.OutputFilenameTile, - self.OutputDirectory, - self.AssetDependency - ]: - serialized.update(attribute.serialize()) - - return serialized - - def update(self, data): - """Update instance with data dict""" - for key, value in data.items(): - setattr(self, key, value) - - def add_render_job_env_var(self): - """Check if in OP or AYON mode and use appropriate env var.""" - self.EnvironmentKeyValue["AYON_RENDER_JOB"] = "1" - self.EnvironmentKeyValue["AYON_BUNDLE_NAME"] = ( - os.environ["AYON_BUNDLE_NAME"]) - - -@six.add_metaclass(AbstractMetaInstancePlugin) -class AbstractSubmitDeadline(pyblish.api.InstancePlugin, - AYONPyblishPluginMixin): - """Class abstracting access to Deadline.""" - - label = "Submit to Deadline" - order = pyblish.api.IntegratorOrder + 0.1 - - import_reference = False - use_published = True - asset_dependencies = False - default_priority = 50 - - def __init__(self, *args, **kwargs): - super(AbstractSubmitDeadline, self).__init__(*args, **kwargs) - self._instance = None - self._deadline_url = None - self.scene_path = None - self.job_info = None - self.plugin_info = None - self.aux_files = None - - def process(self, instance): - """Plugin entry point.""" - self._instance = instance - context = instance.context - self._deadline_url = instance.data["deadline"]["url"] - - assert self._deadline_url, "Requires Deadline Webservice URL" - - file_path = None - if self.use_published: - if not self.import_reference: - file_path = self.from_published_scene() - else: - self.log.info("use the scene with imported reference for rendering") # noqa - file_path = context.data["currentFile"] - - # fallback if nothing was set - if not file_path: - self.log.warning("Falling back to workfile") - file_path = context.data["currentFile"] - - self.scene_path = file_path - self.log.info("Using {} for render/export.".format(file_path)) - - self.job_info = self.get_job_info() - self.plugin_info = self.get_plugin_info() - self.aux_files = self.get_aux_files() - - job_id = self.process_submission() - self.log.info("Submitted job to Deadline: {}.".format(job_id)) - - # TODO: Find a way that's more generic and not render type specific - if instance.data.get("splitRender"): - self.log.info("Splitting export and render in two jobs") - self.log.info("Export job id: %s", job_id) - render_job_info = self.get_job_info(dependency_job_ids=[job_id]) - render_plugin_info = self.get_plugin_info(job_type="render") - payload = self.assemble_payload( - job_info=render_job_info, - plugin_info=render_plugin_info - ) - auth = instance.data["deadline"]["auth"] - verify = instance.data["deadline"]["verify"] - render_job_id = self.submit(payload, auth, verify) - self.log.info("Render job id: %s", render_job_id) - - def process_submission(self): - """Process data for submission. - - This takes Deadline JobInfo, PluginInfo, AuxFile, creates payload - from them and submit it do Deadline. - - Returns: - str: Deadline job ID - - """ - payload = self.assemble_payload() - auth = self._instance.data["deadline"]["auth"] - verify = self._instance.data["deadline"]["verify"] - return self.submit(payload, auth, verify) - - @abstractmethod - def get_job_info(self): - """Return filled Deadline JobInfo. - - This is host/plugin specific implementation of how to fill data in. - - See: - :class:`DeadlineJobInfo` - - Returns: - :class:`DeadlineJobInfo`: Filled Deadline JobInfo. - - """ - pass - - @abstractmethod - def get_plugin_info(self): - """Return filled Deadline PluginInfo. - - This is host/plugin specific implementation of how to fill data in. - - See: - :class:`DeadlineJobInfo` - - Returns: - dict: Filled Deadline JobInfo. - - """ - pass - - def get_aux_files(self): - """Return list of auxiliary files for Deadline job. - - If needed this should be overridden, otherwise return empty list as - that field even empty must be present on Deadline submission. - - Returns: - list: List of files. - - """ - return [] - - def from_published_scene(self, replace_in_path=True): - """Switch work scene for published scene. - - If rendering/exporting from published scenes is enabled, this will - replace paths from working scene to published scene. - - Args: - replace_in_path (bool): if True, it will try to find - old scene name in path of expected files and replace it - with name of published scene. - - Returns: - str: Published scene path. - None: if no published scene is found. - - Note: - Published scene path is actually determined from project Anatomy - as at the time this plugin is running scene can still no be - published. - - """ - return replace_with_published_scene_path( - self._instance, replace_in_path=replace_in_path) - - def assemble_payload( - self, job_info=None, plugin_info=None, aux_files=None): - """Assemble payload data from its various parts. - - Args: - job_info (DeadlineJobInfo): Deadline JobInfo. You can use - :class:`DeadlineJobInfo` for it. - plugin_info (dict): Deadline PluginInfo. Plugin specific options. - aux_files (list, optional): List of auxiliary file to submit with - the job. - - Returns: - dict: Deadline Payload. - - """ - job = job_info or self.job_info - return { - "JobInfo": job.serialize(), - "PluginInfo": plugin_info or self.plugin_info, - "AuxFiles": aux_files or self.aux_files - } - - def submit(self, payload, auth, verify): - """Submit payload to Deadline API end-point. - - This takes payload in the form of JSON file and POST it to - Deadline jobs end-point. - - Args: - payload (dict): dict to become json in deadline submission. - auth (tuple): (username, password) - verify (bool): verify SSL certificate if present - - Returns: - str: resulting Deadline job id. - - Throws: - KnownPublishError: if submission fails. - - """ - url = "{}/api/jobs".format(self._deadline_url) - response = requests_post( - url, json=payload, auth=auth, verify=verify) - if not response.ok: - self.log.error("Submission failed!") - self.log.error(response.status_code) - self.log.error(response.content) - self.log.debug(payload) - raise KnownPublishError(response.text) - - try: - result = response.json() - except JSONDecodeError: - msg = "Broken response {}. ".format(response) - msg += "Try restarting the Deadline Webservice." - self.log.warning(msg, exc_info=True) - raise KnownPublishError("Broken response from DL") - - # for submit publish job - self._instance.data["deadlineSubmissionJob"] = result - - return result["_id"] diff --git a/server_addon/deadline/client/ayon_deadline/addon.py b/server_addon/deadline/client/ayon_deadline/addon.py deleted file mode 100644 index 87fc2ad665..0000000000 --- a/server_addon/deadline/client/ayon_deadline/addon.py +++ /dev/null @@ -1,81 +0,0 @@ -import os -import sys - -import requests -import six - -from ayon_core.lib import Logger -from ayon_core.addon import AYONAddon, IPluginPaths - -from .version import __version__ - - -class DeadlineWebserviceError(Exception): - """ - Exception to throw when connection to Deadline server fails. - """ - - -class DeadlineAddon(AYONAddon, IPluginPaths): - name = "deadline" - version = __version__ - - def initialize(self, studio_settings): - deadline_settings = studio_settings[self.name] - deadline_servers_info = { - url_item["name"]: url_item - for url_item in deadline_settings["deadline_urls"] - } - - if not deadline_servers_info: - self.enabled = False - self.log.warning(( - "Deadline Webservice URLs are not specified. Disabling addon." - )) - - self.deadline_servers_info = deadline_servers_info - - def get_plugin_paths(self): - """Deadline plugin paths.""" - current_dir = os.path.dirname(os.path.abspath(__file__)) - return { - "publish": [os.path.join(current_dir, "plugins", "publish")] - } - - @staticmethod - def get_deadline_pools(webservice, auth=None, log=None): - """Get pools from Deadline. - Args: - webservice (str): Server url. - auth (Optional[Tuple[str, str]]): Tuple containing username, - password - log (Optional[Logger]): Logger to log errors to, if provided. - Returns: - List[str]: Pools. - Throws: - RuntimeError: If deadline webservice is unreachable. - - """ - from .abstract_submit_deadline import requests_get - - if not log: - log = Logger.get_logger(__name__) - - argument = "{}/api/pools?NamesOnly=true".format(webservice) - try: - kwargs = {} - if auth: - kwargs["auth"] = auth - response = requests_get(argument, **kwargs) - except requests.exceptions.ConnectionError as exc: - msg = 'Cannot connect to DL web service {}'.format(webservice) - log.error(msg) - six.reraise( - DeadlineWebserviceError, - DeadlineWebserviceError('{} - {}'.format(msg, exc)), - sys.exc_info()[2]) - if not response.ok: - log.warning("No pools retrieved") - return [] - - return response.json() diff --git a/server_addon/deadline/client/ayon_deadline/lib.py b/server_addon/deadline/client/ayon_deadline/lib.py deleted file mode 100644 index 7f07c350ec..0000000000 --- a/server_addon/deadline/client/ayon_deadline/lib.py +++ /dev/null @@ -1,10 +0,0 @@ -# describes list of product typed used for plugin filtering for farm publishing -FARM_FAMILIES = [ - "render", "render.farm", "render.frames_farm", - "prerender", "prerender.farm", "prerender.frames_farm", - "renderlayer", "imagesequence", "image", - "vrayscene", "maxrender", - "arnold_rop", "mantra_rop", - "karma_rop", "vray_rop", "redshift_rop", - "renderFarm", "usdrender", "publish.hou" -] diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_deadline_server_from_instance.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_deadline_server_from_instance.py deleted file mode 100644 index 2c8cbd1620..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_deadline_server_from_instance.py +++ /dev/null @@ -1,115 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect Deadline servers from instance. - -This is resolving index of server lists stored in `deadlineServers` instance -attribute or using default server if that attribute doesn't exists. - -""" -import pyblish.api -from ayon_core.pipeline.publish import KnownPublishError - -from ayon_deadline.lib import FARM_FAMILIES - - -class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin): - """Collect Deadline Webservice URL from instance.""" - - # Run before collect_render. - order = pyblish.api.CollectorOrder + 0.225 - label = "Deadline Webservice from the Instance" - targets = ["local"] - - families = FARM_FAMILIES - - def process(self, instance): - if not instance.data.get("farm"): - self.log.debug("Should not be processed on farm, skipping.") - return - - if not instance.data.get("deadline"): - instance.data["deadline"] = {} - - # todo: separate logic should be removed, all hosts should have same - host_name = instance.context.data["hostName"] - if host_name == "maya": - deadline_url = self._collect_deadline_url(instance) - else: - deadline_url = (instance.data.get("deadlineUrl") or # backwards - instance.data.get("deadline", {}).get("url")) - if deadline_url: - instance.data["deadline"]["url"] = deadline_url.strip().rstrip("/") - else: - instance.data["deadline"]["url"] = instance.context.data["deadline"]["defaultUrl"] # noqa - self.log.debug( - "Using {} for submission".format(instance.data["deadline"]["url"])) - - def _collect_deadline_url(self, render_instance): - # type: (pyblish.api.Instance) -> str - """Get Deadline Webservice URL from render instance. - - This will get all configured Deadline Webservice URLs and create - subset of them based upon project configuration. It will then take - `deadlineServers` from render instance that is now basically `int` - index of that list. - - Args: - render_instance (pyblish.api.Instance): Render instance created - by Creator in Maya. - - Returns: - str: Selected Deadline Webservice URL. - - """ - # Not all hosts can import this module. - from maya import cmds - deadline_settings = ( - render_instance.context.data - ["project_settings"] - ["deadline"] - ) - default_server_url = (render_instance.context.data["deadline"] - ["defaultUrl"]) - # QUESTION How and where is this is set? Should be removed? - instance_server = render_instance.data.get("deadlineServers") - if not instance_server: - self.log.debug("Using default server.") - return default_server_url - - # Get instance server as sting. - if isinstance(instance_server, int): - instance_server = cmds.getAttr( - "{}.deadlineServers".format(render_instance.data["objset"]), - asString=True - ) - - default_servers = { - url_item["name"]: url_item["value"] - for url_item in deadline_settings["deadline_servers_info"] - } - project_servers = ( - render_instance.context.data - ["project_settings"] - ["deadline"] - ["deadline_servers"] - ) - if not project_servers: - self.log.debug("Not project servers found. Using default servers.") - return default_servers[instance_server] - - project_enabled_servers = { - k: default_servers[k] - for k in project_servers - if k in default_servers - } - - if instance_server not in project_enabled_servers: - msg = ( - "\"{}\" server on instance is not enabled in project settings." - " Enabled project servers:\n{}".format( - instance_server, project_enabled_servers - ) - ) - raise KnownPublishError(msg) - - self.log.debug("Using project approved server.") - return project_enabled_servers[instance_server] diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_default_deadline_server.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_default_deadline_server.py deleted file mode 100644 index 77d03c713f..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_default_deadline_server.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect default Deadline server.""" -import pyblish.api - - -class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): - """Collect default Deadline Webservice URL. - - DL webservice addresses must be configured first in System Settings for - project settings enum to work. - - Default webservice could be overridden by - `project_settings/deadline/deadline_servers`. Currently only single url - is expected. - - This url could be overridden by some hosts directly on instances with - `CollectDeadlineServerFromInstance`. - """ - - # Run before collect_deadline_server_instance. - order = pyblish.api.CollectorOrder + 0.200 - label = "Default Deadline Webservice" - targets = ["local"] - - def process(self, context): - try: - deadline_addon = context.data["ayonAddonsManager"]["deadline"] - except AttributeError: - self.log.error("Cannot get AYON Deadline addon.") - raise AssertionError("AYON Deadline addon not found.") - - deadline_settings = context.data["project_settings"]["deadline"] - deadline_server_name = deadline_settings["deadline_server"] - - dl_server_info = None - if deadline_server_name: - dl_server_info = deadline_addon.deadline_servers_info.get( - deadline_server_name) - - if dl_server_info: - deadline_url = dl_server_info["value"] - else: - default_dl_server_info = deadline_addon.deadline_servers_info[0] - deadline_url = default_dl_server_info["value"] - - context.data["deadline"] = {} - context.data["deadline"]["defaultUrl"] = ( - deadline_url.strip().rstrip("/")) diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_pools.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_pools.py deleted file mode 100644 index b2b6bc60d4..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_pools.py +++ /dev/null @@ -1,91 +0,0 @@ -# -*- coding: utf-8 -*- -import pyblish.api -from ayon_core.lib import TextDef -from ayon_core.pipeline.publish import AYONPyblishPluginMixin - -from ayon_deadline.lib import FARM_FAMILIES - - -class CollectDeadlinePools(pyblish.api.InstancePlugin, - AYONPyblishPluginMixin): - """Collect pools from instance or Publisher attributes, from Setting - otherwise. - - Pools are used to control which DL workers could render the job. - - Pools might be set: - - directly on the instance (set directly in DCC) - - from Publisher attributes - - from defaults from Settings. - - Publisher attributes could be shown even for instances that should be - rendered locally as visibility is driven by product type of the instance - (which will be `render` most likely). - (Might be resolved in the future and class attribute 'families' should - be cleaned up.) - - """ - - order = pyblish.api.CollectorOrder + 0.420 - label = "Collect Deadline Pools" - hosts = [ - "aftereffects", - "fusion", - "harmony", - "maya", - "max", - "houdini", - "nuke", - ] - - families = FARM_FAMILIES - - primary_pool = None - secondary_pool = None - - @classmethod - def apply_settings(cls, project_settings): - # deadline.publish.CollectDeadlinePools - settings = project_settings["deadline"]["publish"]["CollectDeadlinePools"] # noqa - cls.primary_pool = settings.get("primary_pool", None) - cls.secondary_pool = settings.get("secondary_pool", None) - - def process(self, instance): - attr_values = self.get_attr_values_from_data(instance.data) - if not instance.data.get("primaryPool"): - instance.data["primaryPool"] = ( - attr_values.get("primaryPool") or self.primary_pool or "none" - ) - if instance.data["primaryPool"] == "-": - instance.data["primaryPool"] = None - - if not instance.data.get("secondaryPool"): - instance.data["secondaryPool"] = ( - attr_values.get("secondaryPool") or self.secondary_pool or "none" # noqa - ) - - if instance.data["secondaryPool"] == "-": - instance.data["secondaryPool"] = None - - @classmethod - def get_attribute_defs(cls): - # TODO: Preferably this would be an enum for the user - # but the Deadline server URL can be dynamic and - # can be set per render instance. Since get_attribute_defs - # can't be dynamic unfortunately EnumDef isn't possible (yet?) - # pool_names = self.deadline_addon.get_deadline_pools(deadline_url, - # self.log) - # secondary_pool_names = ["-"] + pool_names - - return [ - TextDef("primaryPool", - label="Primary Pool", - default=cls.primary_pool, - tooltip="Deadline primary pool, " - "applicable for farm rendering"), - TextDef("secondaryPool", - label="Secondary Pool", - default=cls.secondary_pool, - tooltip="Deadline secondary pool, " - "applicable for farm rendering") - ] diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_user_credentials.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_user_credentials.py deleted file mode 100644 index 1c59c178d3..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_user_credentials.py +++ /dev/null @@ -1,98 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect user credentials - -Requires: - context -> project_settings - instance.data["deadline"]["url"] - -Provides: - instance.data["deadline"] -> require_authentication (bool) - instance.data["deadline"] -> auth (tuple (str, str)) - - (username, password) or None -""" -import pyblish.api - -from ayon_api import get_server_api_connection - -from ayon_deadline.lib import FARM_FAMILIES - - -class CollectDeadlineUserCredentials(pyblish.api.InstancePlugin): - """Collects user name and password for artist if DL requires authentication - - If Deadline server is marked to require authentication, it looks first for - default values in 'Studio Settings', which could be overriden by artist - dependent values from 'Site settings`. - """ - order = pyblish.api.CollectorOrder + 0.250 - label = "Collect Deadline User Credentials" - - targets = ["local"] - hosts = ["aftereffects", - "blender", - "fusion", - "harmony", - "nuke", - "maya", - "max", - "houdini"] - - families = FARM_FAMILIES - - def process(self, instance): - if not instance.data.get("farm"): - self.log.debug("Should not be processed on farm, skipping.") - return - - collected_deadline_url = instance.data["deadline"]["url"] - if not collected_deadline_url: - raise ValueError("Instance doesn't have '[deadline][url]'.") - context_data = instance.context.data - deadline_settings = context_data["project_settings"]["deadline"] - - deadline_server_name = None - # deadline url might be set directly from instance, need to find - # metadata for it - for deadline_info in deadline_settings["deadline_urls"]: - dl_settings_url = deadline_info["value"].strip().rstrip("/") - if dl_settings_url == collected_deadline_url: - deadline_server_name = deadline_info["name"] - break - - if not deadline_server_name: - raise ValueError(f"Collected {collected_deadline_url} doesn't " - "match any site configured in Studio Settings") - - instance.data["deadline"]["require_authentication"] = ( - deadline_info["require_authentication"] - ) - instance.data["deadline"]["auth"] = None - - instance.data["deadline"]["verify"] = ( - not deadline_info["not_verify_ssl"]) - - if not deadline_info["require_authentication"]: - return - - addons_manager = instance.context.data["ayonAddonsManager"] - deadline_addon = addons_manager["deadline"] - - default_username = deadline_info["default_username"] - default_password = deadline_info["default_password"] - if default_username and default_password: - self.log.debug("Setting credentials from defaults") - instance.data["deadline"]["auth"] = (default_username, - default_password) - - # TODO import 'get_addon_site_settings' when available - # in public 'ayon_api' - local_settings = get_server_api_connection().get_addon_site_settings( - deadline_addon.name, deadline_addon.version) - local_settings = local_settings["local_settings"] - for server_info in local_settings: - if deadline_server_name == server_info["server_name"]: - if server_info["username"] and server_info["password"]: - self.log.debug("Setting credentials from Site Settings") - instance.data["deadline"]["auth"] = \ - (server_info["username"], server_info["password"]) - break diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/help/validate_deadline_connection.xml b/server_addon/deadline/client/ayon_deadline/plugins/publish/help/validate_deadline_connection.xml deleted file mode 100644 index eec05df08a..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/help/validate_deadline_connection.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - Deadline Authentication - -## Deadline authentication is required - -This project has set in Settings that Deadline requires authentication. - -### How to repair? - -Please go to Ayon Server > Site Settings and provide your Deadline username and password. -In some cases the password may be empty if Deadline is configured to allow that. Ask your administrator. - - - - \ No newline at end of file diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/help/validate_deadline_pools.xml b/server_addon/deadline/client/ayon_deadline/plugins/publish/help/validate_deadline_pools.xml deleted file mode 100644 index 879adcee97..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/help/validate_deadline_pools.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - Deadline Pools - -## Invalid Deadline pools found - -Configured pools don't match available pools in Deadline. - -### How to repair? - -If your instance had deadline pools set on creation, remove or -change them. - -In other cases inform admin to change them in Settings. - -Available deadline pools: - -{pools_str} - - - -### __Detailed Info__ - -This error is shown when a configured pool is not available on Deadline. It -can happen when publishing old workfiles which were created with previous -deadline pools, or someone changed the available pools in Deadline, -but didn't modify AYON Settings to match the changes. - - - \ No newline at end of file diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_aftereffects_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_aftereffects_deadline.py deleted file mode 100644 index 45d907cbba..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_aftereffects_deadline.py +++ /dev/null @@ -1,143 +0,0 @@ -import os -import attr -import getpass -import pyblish.api -from datetime import datetime - -from ayon_core.lib import ( - env_value_to_bool, - collect_frames, - is_in_tests, -) -from ayon_deadline import abstract_submit_deadline -from ayon_deadline.abstract_submit_deadline import DeadlineJobInfo - - -@attr.s -class DeadlinePluginInfo(): - Comp = attr.ib(default=None) - SceneFile = attr.ib(default=None) - OutputFilePath = attr.ib(default=None) - Output = attr.ib(default=None) - StartupDirectory = attr.ib(default=None) - Arguments = attr.ib(default=None) - ProjectPath = attr.ib(default=None) - AWSAssetFile0 = attr.ib(default=None) - Version = attr.ib(default=None) - MultiProcess = attr.ib(default=None) - - -class AfterEffectsSubmitDeadline( - abstract_submit_deadline.AbstractSubmitDeadline -): - - label = "Submit AE to Deadline" - order = pyblish.api.IntegratorOrder + 0.1 - hosts = ["aftereffects"] - families = ["render.farm"] # cannot be "render' as that is integrated - use_published = True - targets = ["local"] - - priority = 50 - chunk_size = 1000000 - group = None - department = None - multiprocess = True - - def get_job_info(self): - dln_job_info = DeadlineJobInfo(Plugin="AfterEffects") - - context = self._instance.context - - batch_name = os.path.basename(self._instance.data["source"]) - if is_in_tests(): - batch_name += datetime.now().strftime("%d%m%Y%H%M%S") - dln_job_info.Name = self._instance.data["name"] - dln_job_info.BatchName = batch_name - dln_job_info.Plugin = "AfterEffects" - dln_job_info.UserName = context.data.get( - "deadlineUser", getpass.getuser()) - # Deadline requires integers in frame range - frame_range = "{}-{}".format( - int(round(self._instance.data["frameStart"])), - int(round(self._instance.data["frameEnd"]))) - dln_job_info.Frames = frame_range - - dln_job_info.Priority = self.priority - dln_job_info.Pool = self._instance.data.get("primaryPool") - dln_job_info.SecondaryPool = self._instance.data.get("secondaryPool") - dln_job_info.Group = self.group - dln_job_info.Department = self.department - dln_job_info.ChunkSize = self.chunk_size - dln_job_info.OutputFilename += \ - os.path.basename(self._instance.data["expectedFiles"][0]) - dln_job_info.OutputDirectory += \ - os.path.dirname(self._instance.data["expectedFiles"][0]) - dln_job_info.JobDelay = "00:00:00" - - keys = [ - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_LOG_NO_COLORS", - "AYON_IN_TESTS" - ] - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - for key in keys: - value = environment.get(key) - if value: - dln_job_info.EnvironmentKeyValue[key] = value - - # to recognize render jobs - dln_job_info.add_render_job_env_var() - - return dln_job_info - - def get_plugin_info(self): - deadline_plugin_info = DeadlinePluginInfo() - - render_path = self._instance.data["expectedFiles"][0] - - file_name, frame = list(collect_frames([render_path]).items())[0] - if frame: - # replace frame ('000001') with Deadline's required '[#######]' - # expects filename in format project_folder_product_version.FRAME.ext - render_dir = os.path.dirname(render_path) - file_name = os.path.basename(render_path) - hashed = '[{}]'.format(len(frame) * "#") - file_name = file_name.replace(frame, hashed) - render_path = os.path.join(render_dir, file_name) - - deadline_plugin_info.Comp = self._instance.data["comp_name"] - deadline_plugin_info.Version = self._instance.data["app_version"] - # must be here because of DL AE plugin - # added override of multiprocess by env var, if shouldn't be used for - # some app variant use MULTIPROCESS:false in Settings, default is True - env_multi = env_value_to_bool("MULTIPROCESS", default=True) - deadline_plugin_info.MultiProcess = env_multi and self.multiprocess - deadline_plugin_info.SceneFile = self.scene_path - deadline_plugin_info.Output = render_path.replace("\\", "/") - - return attr.asdict(deadline_plugin_info) - - def from_published_scene(self): - """ Do not overwrite expected files. - - Use published is set to True, so rendering will be triggered - from published scene (in 'publish' folder). Default implementation - of abstract class renames expected (eg. rendered) files accordingly - which is not needed here. - """ - return super().from_published_scene(False) diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_blender_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_blender_deadline.py deleted file mode 100644 index 073de909b4..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_blender_deadline.py +++ /dev/null @@ -1,225 +0,0 @@ -# -*- coding: utf-8 -*- -"""Submitting render job to Deadline.""" - -import os -import getpass -import attr -from datetime import datetime - -from ayon_core.lib import ( - BoolDef, - NumberDef, - TextDef, - is_in_tests, -) -from ayon_core.pipeline.publish import AYONPyblishPluginMixin -from ayon_core.pipeline.farm.tools import iter_expected_files - -from ayon_deadline import abstract_submit_deadline -from ayon_deadline.abstract_submit_deadline import DeadlineJobInfo - - -@attr.s -class BlenderPluginInfo(): - SceneFile = attr.ib(default=None) # Input - Version = attr.ib(default=None) # Mandatory for Deadline - SaveFile = attr.ib(default=True) - - -class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, - AYONPyblishPluginMixin): - label = "Submit Render to Deadline" - hosts = ["blender"] - families = ["render"] - settings_category = "deadline" - - use_published = True - priority = 50 - chunk_size = 1 - jobInfo = {} - pluginInfo = {} - group = None - job_delay = "00:00:00:00" - - def get_job_info(self): - job_info = DeadlineJobInfo(Plugin="Blender") - - job_info.update(self.jobInfo) - - instance = self._instance - context = instance.context - - # Always use the original work file name for the Job name even when - # rendering is done from the published Work File. The original work - # file name is clearer because it can also have subversion strings, - # etc. which are stripped for the published file. - src_filepath = context.data["currentFile"] - src_filename = os.path.basename(src_filepath) - - if is_in_tests(): - src_filename += datetime.now().strftime("%d%m%Y%H%M%S") - - job_info.Name = f"{src_filename} - {instance.name}" - job_info.BatchName = src_filename - instance.data.get("blenderRenderPlugin", "Blender") - job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) - - # Deadline requires integers in frame range - frames = "{start}-{end}x{step}".format( - start=int(instance.data["frameStartHandle"]), - end=int(instance.data["frameEndHandle"]), - step=int(instance.data["byFrameStep"]), - ) - job_info.Frames = frames - - job_info.Pool = instance.data.get("primaryPool") - job_info.SecondaryPool = instance.data.get("secondaryPool") - job_info.Comment = instance.data.get("comment") - - if self.group != "none" and self.group: - job_info.Group = self.group - - attr_values = self.get_attr_values_from_data(instance.data) - render_globals = instance.data.setdefault("renderGlobals", {}) - machine_list = attr_values.get("machineList", "") - if machine_list: - if attr_values.get("whitelist", True): - machine_list_key = "Whitelist" - else: - machine_list_key = "Blacklist" - render_globals[machine_list_key] = machine_list - - job_info.ChunkSize = attr_values.get("chunkSize", self.chunk_size) - job_info.Priority = attr_values.get("priority", self.priority) - job_info.ScheduledType = "Once" - job_info.JobDelay = attr_values.get("job_delay", self.job_delay) - - # Add options from RenderGlobals - render_globals = instance.data.get("renderGlobals", {}) - job_info.update(render_globals) - - keys = [ - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "OPENPYPE_SG_USER", - "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_IN_TESTS" - ] - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - - for key in keys: - value = environment.get(key) - if not value: - continue - job_info.EnvironmentKeyValue[key] = value - - # to recognize job from PYPE for turning Event On/Off - job_info.add_render_job_env_var() - job_info.EnvironmentKeyValue["AYON_LOG_NO_COLORS"] = "1" - - # Adding file dependencies. - if self.asset_dependencies: - dependencies = instance.context.data["fileDependencies"] - for dependency in dependencies: - job_info.AssetDependency += dependency - - # Add list of expected files to job - # --------------------------------- - exp = instance.data.get("expectedFiles") - for filepath in iter_expected_files(exp): - job_info.OutputDirectory += os.path.dirname(filepath) - job_info.OutputFilename += os.path.basename(filepath) - - return job_info - - def get_plugin_info(self): - # Not all hosts can import this module. - import bpy - - plugin_info = BlenderPluginInfo( - SceneFile=self.scene_path, - Version=bpy.app.version_string, - SaveFile=True, - ) - - plugin_payload = attr.asdict(plugin_info) - - # Patching with pluginInfo from settings - for key, value in self.pluginInfo.items(): - plugin_payload[key] = value - - return plugin_payload - - def process_submission(self, auth=None): - instance = self._instance - - expected_files = instance.data["expectedFiles"] - if not expected_files: - raise RuntimeError("No Render Elements found!") - - first_file = next(iter_expected_files(expected_files)) - output_dir = os.path.dirname(first_file) - instance.data["outputDir"] = output_dir - instance.data["toBeRenderedOn"] = "deadline" - - payload = self.assemble_payload() - auth = self._instance.data["deadline"]["auth"] - verify = self._instance.data["deadline"]["verify"] - return self.submit(payload, auth=auth, verify=verify) - - def from_published_scene(self): - """ - This is needed to set the correct path for the json metadata. Because - the rendering path is set in the blend file during the collection, - and the path is adjusted to use the published scene, this ensures that - the metadata and the rendered files are in the same location. - """ - return super().from_published_scene(False) - - @classmethod - def get_attribute_defs(cls): - defs = super(BlenderSubmitDeadline, cls).get_attribute_defs() - defs.extend([ - BoolDef("use_published", - default=cls.use_published, - label="Use Published Scene"), - - NumberDef("priority", - minimum=1, - maximum=250, - decimals=0, - default=cls.priority, - label="Priority"), - - NumberDef("chunkSize", - minimum=1, - maximum=50, - decimals=0, - default=cls.chunk_size, - label="Frame Per Task"), - - TextDef("group", - default=cls.group, - label="Group Name"), - - TextDef("job_delay", - default=cls.job_delay, - label="Job Delay", - placeholder="dd:hh:mm:ss", - tooltip="Delay the job by the specified amount of time. " - "Timecode: dd:hh:mm:ss."), - ]) - - return defs diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_celaction_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_celaction_deadline.py deleted file mode 100644 index e9313e3f2f..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_celaction_deadline.py +++ /dev/null @@ -1,271 +0,0 @@ -import os -import re -import json -import getpass -import pyblish.api - -from ayon_deadline.abstract_submit_deadline import requests_post - - -class CelactionSubmitDeadline(pyblish.api.InstancePlugin): - """Submit CelAction2D scene to Deadline - - Renders are submitted to a Deadline Web Service. - - """ - - label = "Submit CelAction to Deadline" - order = pyblish.api.IntegratorOrder + 0.1 - hosts = ["celaction"] - families = ["render.farm"] - settings_category = "deadline" - - deadline_department = "" - deadline_priority = 50 - deadline_pool = "" - deadline_pool_secondary = "" - deadline_group = "" - deadline_chunk_size = 1 - deadline_job_delay = "00:00:08:00" - - def process(self, instance): - - context = instance.context - - deadline_url = instance.data["deadline"]["url"] - assert deadline_url, "Requires Deadline Webservice URL" - - self.deadline_url = "{}/api/jobs".format(deadline_url) - self._comment = instance.data["comment"] - self._deadline_user = context.data.get( - "deadlineUser", getpass.getuser()) - self._frame_start = int(instance.data["frameStart"]) - self._frame_end = int(instance.data["frameEnd"]) - - # get output path - render_path = instance.data['path'] - script_path = context.data["currentFile"] - - response = self.payload_submit(instance, - script_path, - render_path - ) - # Store output dir for unified publisher (filesequence) - instance.data["deadlineSubmissionJob"] = response.json() - - instance.data["outputDir"] = os.path.dirname( - render_path).replace("\\", "/") - - instance.data["publishJobState"] = "Suspended" - - # adding 2d render specific family for version identification in Loader - instance.data["families"] = ["render2d"] - - def payload_submit(self, - instance, - script_path, - render_path - ): - resolution_width = instance.data["resolutionWidth"] - resolution_height = instance.data["resolutionHeight"] - render_dir = os.path.normpath(os.path.dirname(render_path)) - render_path = os.path.normpath(render_path) - script_name = os.path.basename(script_path) - - anatomy = instance.context.data["anatomy"] - publish_template = anatomy.get_template_item( - "publish", "default", "path" - ) - for item in instance.context: - if "workfile" in item.data["productType"]: - msg = "Workfile (scene) must be published along" - assert item.data["publish"] is True, msg - - template_data = item.data.get("anatomyData") - rep = item.data.get("representations")[0].get("name") - template_data["representation"] = rep - template_data["ext"] = rep - template_data["comment"] = None - template_filled = publish_template.format_strict( - template_data - ) - script_path = os.path.normpath(template_filled) - - self.log.info( - "Using published scene for render {}".format(script_path) - ) - - jobname = "%s - %s" % (script_name, instance.name) - - output_filename_0 = self.preview_fname(render_path) - - try: - # Ensure render folder exists - os.makedirs(render_dir) - except OSError: - pass - - # define chunk and priority - chunk_size = instance.context.data.get("chunk") - if not chunk_size: - chunk_size = self.deadline_chunk_size - - # search for %02d pattern in name, and padding number - search_results = re.search(r"(%0)(\d)(d)[._]", render_path).groups() - split_patern = "".join(search_results) - padding_number = int(search_results[1]) - - args = [ - f"{script_path}", - "-a", - "-16", - "-s ", - "-e ", - f"-d {render_dir}", - f"-x {resolution_width}", - f"-y {resolution_height}", - f"-r {render_path.replace(split_patern, '')}", - f"-= AbsoluteFrameNumber=on -= PadDigits={padding_number}", - "-= ClearAttachment=on", - ] - - payload = { - "JobInfo": { - # Job name, as seen in Monitor - "Name": jobname, - - # plugin definition - "Plugin": "CelAction", - - # Top-level group name - "BatchName": script_name, - - # Arbitrary username, for visualisation in Monitor - "UserName": self._deadline_user, - - "Department": self.deadline_department, - "Priority": self.deadline_priority, - - "Group": self.deadline_group, - "Pool": self.deadline_pool, - "SecondaryPool": self.deadline_pool_secondary, - "ChunkSize": chunk_size, - - "Frames": f"{self._frame_start}-{self._frame_end}", - "Comment": self._comment, - - # Optional, enable double-click to preview rendered - # frames from Deadline Monitor - "OutputFilename0": output_filename_0.replace("\\", "/"), - - # # Asset dependency to wait for at least - # the scene file to sync. - # "AssetDependency0": script_path - "ScheduledType": "Once", - "JobDelay": self.deadline_job_delay - }, - "PluginInfo": { - # Input - "SceneFile": script_path, - - # Output directory - "OutputFilePath": render_dir.replace("\\", "/"), - - # Plugin attributes - "StartupDirectory": "", - "Arguments": " ".join(args), - - # Resolve relative references - "ProjectPath": script_path, - "AWSAssetFile0": render_path, - }, - - # Mandatory for Deadline, may be empty - "AuxFiles": [] - } - - plugin = payload["JobInfo"]["Plugin"] - self.log.debug("using render plugin : {}".format(plugin)) - - self.log.debug("Submitting..") - self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) - - # adding expectied files to instance.data - self.expected_files(instance, render_path) - self.log.debug("__ expectedFiles: `{}`".format( - instance.data["expectedFiles"])) - auth = instance.data["deadline"]["auth"] - verify = instance.data["deadline"]["verify"] - response = requests_post(self.deadline_url, json=payload, - auth=auth, - verify=verify) - - if not response.ok: - self.log.error( - "Submission failed! [{}] {}".format( - response.status_code, response.content)) - self.log.debug(payload) - raise SystemExit(response.text) - - return response - - def preflight_check(self, instance): - """Ensure the startFrame, endFrame and byFrameStep are integers""" - - for key in ("frameStart", "frameEnd"): - value = instance.data[key] - - if int(value) == value: - continue - - self.log.warning( - "%f=%d was rounded off to nearest integer" - % (value, int(value)) - ) - - def preview_fname(self, path): - """Return output file path with #### for padding. - - Deadline requires the path to be formatted with # in place of numbers. - For example `/path/to/render.####.png` - - Args: - path (str): path to rendered images - - Returns: - str - - """ - self.log.debug("_ path: `{}`".format(path)) - if "%" in path: - search_results = re.search(r"[._](%0)(\d)(d)[._]", path).groups() - split_patern = "".join(search_results) - split_path = path.split(split_patern) - hashes = "#" * int(search_results[1]) - return "".join([split_path[0], hashes, split_path[-1]]) - - self.log.debug("_ path: `{}`".format(path)) - return path - - def expected_files(self, instance, filepath): - """ Create expected files in instance data - """ - if not instance.data.get("expectedFiles"): - instance.data["expectedFiles"] = [] - - dirpath = os.path.dirname(filepath) - filename = os.path.basename(filepath) - - if "#" in filename: - pparts = filename.split("#") - padding = "%0{}d".format(len(pparts) - 1) - filename = pparts[0] + padding + pparts[-1] - - if "%" not in filename: - instance.data["expectedFiles"].append(filepath) - return - - for i in range(self._frame_start, (self._frame_end + 1)): - instance.data["expectedFiles"].append( - os.path.join(dirpath, (filename % i)).replace("\\", "/") - ) diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_fusion_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_fusion_deadline.py deleted file mode 100644 index bf9df40edc..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_fusion_deadline.py +++ /dev/null @@ -1,253 +0,0 @@ -import os -import json -import getpass - -import pyblish.api - -from ayon_deadline.abstract_submit_deadline import requests_post -from ayon_core.pipeline.publish import ( - AYONPyblishPluginMixin -) -from ayon_core.lib import NumberDef - - -class FusionSubmitDeadline( - pyblish.api.InstancePlugin, - AYONPyblishPluginMixin -): - """Submit current Comp to Deadline - - Renders are submitted to a Deadline Web Service as - supplied via settings key "DEADLINE_REST_URL". - - """ - - label = "Submit Fusion to Deadline" - order = pyblish.api.IntegratorOrder - hosts = ["fusion"] - families = ["render"] - targets = ["local"] - settings_category = "deadline" - - # presets - plugin = None - - priority = 50 - chunk_size = 1 - concurrent_tasks = 1 - group = "" - - @classmethod - def get_attribute_defs(cls): - return [ - NumberDef( - "priority", - label="Priority", - default=cls.priority, - decimals=0 - ), - NumberDef( - "chunk", - label="Frames Per Task", - default=cls.chunk_size, - decimals=0, - minimum=1, - maximum=1000 - ), - NumberDef( - "concurrency", - label="Concurrency", - default=cls.concurrent_tasks, - decimals=0, - minimum=1, - maximum=10 - ) - ] - - def process(self, instance): - if not instance.data.get("farm"): - self.log.debug("Skipping local instance.") - return - - attribute_values = self.get_attr_values_from_data( - instance.data) - - context = instance.context - - key = "__hasRun{}".format(self.__class__.__name__) - if context.data.get(key, False): - return - else: - context.data[key] = True - - from ayon_fusion.api.lib import get_frame_path - - deadline_url = instance.data["deadline"]["url"] - assert deadline_url, "Requires Deadline Webservice URL" - - # Collect all saver instances in context that are to be rendered - saver_instances = [] - for inst in context: - if inst.data["productType"] != "render": - # Allow only saver family instances - continue - - if not inst.data.get("publish", True): - # Skip inactive instances - continue - - self.log.debug(inst.data["name"]) - saver_instances.append(inst) - - if not saver_instances: - raise RuntimeError("No instances found for Deadline submission") - - comment = instance.data.get("comment", "") - deadline_user = context.data.get("deadlineUser", getpass.getuser()) - - script_path = context.data["currentFile"] - - anatomy = instance.context.data["anatomy"] - publish_template = anatomy.get_template_item( - "publish", "default", "path" - ) - for item in context: - if "workfile" in item.data["families"]: - msg = "Workfile (scene) must be published along" - assert item.data["publish"] is True, msg - - template_data = item.data.get("anatomyData") - rep = item.data.get("representations")[0].get("name") - template_data["representation"] = rep - template_data["ext"] = rep - template_data["comment"] = None - template_filled = publish_template.format_strict( - template_data - ) - script_path = os.path.normpath(template_filled) - - self.log.info( - "Using published scene for render {}".format(script_path) - ) - - filename = os.path.basename(script_path) - - # Documentation for keys available at: - # https://docs.thinkboxsoftware.com - # /products/deadline/8.0/1_User%20Manual/manual - # /manual-submission.html#job-info-file-options - payload = { - "JobInfo": { - # Top-level group name - "BatchName": filename, - - # Asset dependency to wait for at least the scene file to sync. - "AssetDependency0": script_path, - - # Job name, as seen in Monitor - "Name": filename, - - "Priority": attribute_values.get( - "priority", self.priority), - "ChunkSize": attribute_values.get( - "chunk", self.chunk_size), - "ConcurrentTasks": attribute_values.get( - "concurrency", - self.concurrent_tasks - ), - - # User, as seen in Monitor - "UserName": deadline_user, - - "Pool": instance.data.get("primaryPool"), - "SecondaryPool": instance.data.get("secondaryPool"), - "Group": self.group, - - "Plugin": self.plugin, - "Frames": "{start}-{end}".format( - start=int(instance.data["frameStartHandle"]), - end=int(instance.data["frameEndHandle"]) - ), - - "Comment": comment, - }, - "PluginInfo": { - # Input - "FlowFile": script_path, - - # Mandatory for Deadline - "Version": str(instance.data["app_version"]), - - # Render in high quality - "HighQuality": True, - - # Whether saver output should be checked after rendering - # is complete - "CheckOutput": True, - - # Proxy: higher numbers smaller images for faster test renders - # 1 = no proxy quality - "Proxy": 1 - }, - - # Mandatory for Deadline, may be empty - "AuxFiles": [] - } - - # Enable going to rendered frames from Deadline Monitor - for index, instance in enumerate(saver_instances): - head, padding, tail = get_frame_path( - instance.data["expectedFiles"][0] - ) - path = "{}{}{}".format(head, "#" * padding, tail) - folder, filename = os.path.split(path) - payload["JobInfo"]["OutputDirectory%d" % index] = folder - payload["JobInfo"]["OutputFilename%d" % index] = filename - - # Include critical variables with submission - keys = [ - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_LOG_NO_COLORS", - "AYON_IN_TESTS", - "AYON_BUNDLE_NAME", - ] - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - - # to recognize render jobs - environment["AYON_RENDER_JOB"] = "1" - - payload["JobInfo"].update({ - "EnvironmentKeyValue%d" % index: "{key}={value}".format( - key=key, - value=environment[key] - ) for index, key in enumerate(environment) - }) - - self.log.debug("Submitting..") - self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) - - # E.g. http://192.168.0.1:8082/api/jobs - url = "{}/api/jobs".format(deadline_url) - auth = instance.data["deadline"]["auth"] - verify = instance.data["deadline"]["verify"] - response = requests_post(url, json=payload, auth=auth, verify=verify) - if not response.ok: - raise Exception(response.text) - - # Store the response for dependent job submission plug-ins - for instance in saver_instances: - instance.data["deadlineSubmissionJob"] = response.json() diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_harmony_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_harmony_deadline.py deleted file mode 100644 index bc91483c4f..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_harmony_deadline.py +++ /dev/null @@ -1,420 +0,0 @@ -# -*- coding: utf-8 -*- -"""Submitting render job to Deadline.""" -import os -from pathlib import Path -from collections import OrderedDict -from zipfile import ZipFile, is_zipfile -import re -from datetime import datetime - -import attr -import pyblish.api - -from ayon_deadline import abstract_submit_deadline -from ayon_deadline.abstract_submit_deadline import DeadlineJobInfo -from ayon_core.lib import is_in_tests - - -class _ZipFile(ZipFile): - """Extended check for windows invalid characters.""" - - # this is extending default zipfile table for few invalid characters - # that can come from Mac - _windows_illegal_characters = ":<>|\"?*\r\n\x00" - _windows_illegal_name_trans_table = str.maketrans( - _windows_illegal_characters, - "_" * len(_windows_illegal_characters) - ) - - -@attr.s -class PluginInfo(object): - """Plugin info structure for Harmony Deadline plugin.""" - - SceneFile = attr.ib() - # Harmony version - Version = attr.ib() - - Camera = attr.ib(default="") - FieldOfView = attr.ib(default=41.11) - IsDatabase = attr.ib(default=False) - ResolutionX = attr.ib(default=1920) - ResolutionY = attr.ib(default=1080) - - # Resolution name preset, default - UsingResPreset = attr.ib(default=False) - ResolutionName = attr.ib(default="HDTV_1080p24") - - PreRenderInlineScript = attr.ib(default=None) - - # -------------------------------------------------- - _outputNode = attr.ib(factory=list) - - @property - def OutputNode(self): # noqa: N802 - """Return all output nodes formatted for Deadline. - - Returns: - dict: as `{'Output0Node', 'Top/renderFarmDefault'}` - - """ - out = {} - for index, v in enumerate(self._outputNode): - out["Output{}Node".format(index)] = v - return out - - @OutputNode.setter - def OutputNode(self, val): # noqa: N802 - self._outputNode.append(val) - - # -------------------------------------------------- - _outputType = attr.ib(factory=list) - - @property - def OutputType(self): # noqa: N802 - """Return output nodes type formatted for Deadline. - - Returns: - dict: as `{'Output0Type', 'Image'}` - - """ - out = {} - for index, v in enumerate(self._outputType): - out["Output{}Type".format(index)] = v - return out - - @OutputType.setter - def OutputType(self, val): # noqa: N802 - self._outputType.append(val) - - # -------------------------------------------------- - _outputLeadingZero = attr.ib(factory=list) - - @property - def OutputLeadingZero(self): # noqa: N802 - """Return output nodes type formatted for Deadline. - - Returns: - dict: as `{'Output0LeadingZero', '3'}` - - """ - out = {} - for index, v in enumerate(self._outputLeadingZero): - out["Output{}LeadingZero".format(index)] = v - return out - - @OutputLeadingZero.setter - def OutputLeadingZero(self, val): # noqa: N802 - self._outputLeadingZero.append(val) - - # -------------------------------------------------- - _outputFormat = attr.ib(factory=list) - - @property - def OutputFormat(self): # noqa: N802 - """Return output nodes format formatted for Deadline. - - Returns: - dict: as `{'Output0Type', 'PNG4'}` - - """ - out = {} - for index, v in enumerate(self._outputFormat): - out["Output{}Format".format(index)] = v - return out - - @OutputFormat.setter - def OutputFormat(self, val): # noqa: N802 - self._outputFormat.append(val) - - # -------------------------------------------------- - _outputStartFrame = attr.ib(factory=list) - - @property - def OutputStartFrame(self): # noqa: N802 - """Return start frame for output nodes formatted for Deadline. - - Returns: - dict: as `{'Output0StartFrame', '1'}` - - """ - out = {} - for index, v in enumerate(self._outputStartFrame): - out["Output{}StartFrame".format(index)] = v - return out - - @OutputStartFrame.setter - def OutputStartFrame(self, val): # noqa: N802 - self._outputStartFrame.append(val) - - # -------------------------------------------------- - _outputPath = attr.ib(factory=list) - - @property - def OutputPath(self): # noqa: N802 - """Return output paths for nodes formatted for Deadline. - - Returns: - dict: as `{'Output0Path', '/output/path'}` - - """ - out = {} - for index, v in enumerate(self._outputPath): - out["Output{}Path".format(index)] = v - return out - - @OutputPath.setter - def OutputPath(self, val): # noqa: N802 - self._outputPath.append(val) - - def set_output(self, node, image_format, output, - output_type="Image", zeros=3, start_frame=1): - """Helper to set output. - - This should be used instead of setting properties individually - as so index remain consistent. - - Args: - node (str): harmony write node name - image_format (str): format of output (PNG4, TIF, ...) - output (str): output path - output_type (str, optional): "Image" or "Movie" (not supported). - zeros (int, optional): Leading zeros (for 0001 = 3) - start_frame (int, optional): Sequence offset. - - """ - - self.OutputNode = node - self.OutputFormat = image_format - self.OutputPath = output - self.OutputType = output_type - self.OutputLeadingZero = zeros - self.OutputStartFrame = start_frame - - def serialize(self): - """Return all data serialized as dictionary. - - Returns: - OrderedDict: all serialized data. - - """ - def filter_data(a, v): - if a.name.startswith("_"): - return False - if v is None: - return False - return True - - serialized = attr.asdict( - self, dict_factory=OrderedDict, filter=filter_data) - serialized.update(self.OutputNode) - serialized.update(self.OutputFormat) - serialized.update(self.OutputPath) - serialized.update(self.OutputType) - serialized.update(self.OutputLeadingZero) - serialized.update(self.OutputStartFrame) - - return serialized - - -class HarmonySubmitDeadline( - abstract_submit_deadline.AbstractSubmitDeadline -): - """Submit render write of Harmony scene to Deadline. - - Renders are submitted to a Deadline Web Service as - supplied via the environment variable ``DEADLINE_REST_URL``. - - Note: - If Deadline configuration is not detected, this plugin will - be disabled. - - Attributes: - use_published (bool): Use published scene to render instead of the - one in work area. - - """ - - label = "Submit to Deadline" - order = pyblish.api.IntegratorOrder + 0.1 - hosts = ["harmony"] - families = ["render.farm"] - targets = ["local"] - settings_category = "deadline" - - optional = True - use_published = False - priority = 50 - chunk_size = 1000000 - group = "none" - department = "" - - def get_job_info(self): - job_info = DeadlineJobInfo("Harmony") - job_info.Name = self._instance.data["name"] - job_info.Plugin = "HarmonyAYON" - job_info.Frames = "{}-{}".format( - self._instance.data["frameStartHandle"], - self._instance.data["frameEndHandle"] - ) - # for now, get those from presets. Later on it should be - # configurable in Harmony UI directly. - job_info.Priority = self.priority - job_info.Pool = self._instance.data.get("primaryPool") - job_info.SecondaryPool = self._instance.data.get("secondaryPool") - job_info.ChunkSize = self.chunk_size - batch_name = os.path.basename(self._instance.data["source"]) - if is_in_tests(): - batch_name += datetime.now().strftime("%d%m%Y%H%M%S") - job_info.BatchName = batch_name - job_info.Department = self.department - job_info.Group = self.group - - keys = [ - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_LOG_NO_COLORS" - "AYON_IN_TESTS" - ] - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - for key in keys: - value = environment.get(key) - if value: - job_info.EnvironmentKeyValue[key] = value - - # to recognize render jobs - job_info.add_render_job_env_var() - - return job_info - - def _unzip_scene_file(self, published_scene: Path) -> Path: - """Unzip scene zip file to its directory. - - Unzip scene file (if it is zip file) to its current directory and - return path to xstage file there. Xstage file is determined by its - name. - - Args: - published_scene (Path): path to zip file. - - Returns: - Path: The path to unzipped xstage. - """ - # if not zip, bail out. - if "zip" not in published_scene.suffix or not is_zipfile( - published_scene.as_posix() - ): - self.log.error("Published scene is not in zip.") - self.log.error(published_scene) - raise AssertionError("invalid scene format") - - xstage_path = ( - published_scene.parent - / published_scene.stem - / f"{published_scene.stem}.xstage" - ) - unzip_dir = (published_scene.parent / published_scene.stem) - with _ZipFile(published_scene, "r") as zip_ref: - # UNC path (//?/) added to minimalize risk with extracting - # to large file paths - zip_ref.extractall("//?/" + str(unzip_dir.as_posix())) - - # find any xstage files in directory, prefer the one with the same name - # as directory (plus extension) - xstage_files = [] - for scene in unzip_dir.iterdir(): - if scene.suffix == ".xstage": - xstage_files.append(scene) - - # there must be at least one (but maybe not more?) xstage file - if not xstage_files: - self.log.error("No xstage files found in zip") - raise AssertionError("Invalid scene archive") - - ideal_scene = False - # find the one with the same name as zip. In case there can be more - # then one xtage file. - for scene in xstage_files: - # if /foo/bar/baz.zip == /foo/bar/baz/baz.xstage - # ^^^ ^^^ - if scene.stem == published_scene.stem: - xstage_path = scene - ideal_scene = True - - # but sometimes xstage file has different name then zip - in that case - # use that one. - if not ideal_scene: - xstage_path = xstage_files[0] - return xstage_path - - def get_plugin_info(self): - # this is path to published scene workfile _ZIP_. Before - # rendering, we need to unzip it. - published_scene = Path( - self.from_published_scene(False)) - self.log.debug(f"Processing {published_scene.as_posix()}") - xstage_path = self._unzip_scene_file(published_scene) - render_path = xstage_path.parent / "renders" - - # for submit_publish job to create .json file in - self._instance.data["outputDir"] = render_path - new_expected_files = [] - render_path_str = str(render_path.as_posix()) - for file in self._instance.data["expectedFiles"]: - _file = str(Path(file).as_posix()) - expected_dir_str = os.path.dirname(_file) - new_expected_files.append( - _file.replace(expected_dir_str, render_path_str) - ) - audio_file = self._instance.data.get("audioFile") - if audio_file: - abs_path = xstage_path.parent / audio_file - self._instance.context.data["audioFile"] = str(abs_path) - - self._instance.data["source"] = str(published_scene.as_posix()) - self._instance.data["expectedFiles"] = new_expected_files - harmony_plugin_info = PluginInfo( - SceneFile=xstage_path.as_posix(), - Version=( - self._instance.context.data["harmonyVersion"].split(".")[0]), - FieldOfView=self._instance.context.data["FOV"], - ResolutionX=self._instance.data["resolutionWidth"], - ResolutionY=self._instance.data["resolutionHeight"] - ) - - pattern = '[0]{' + str(self._instance.data["leadingZeros"]) + \ - '}1\.[a-zA-Z]{3}' - render_prefix = re.sub(pattern, '', - self._instance.data["expectedFiles"][0]) - harmony_plugin_info.set_output( - self._instance.data["setMembers"][0], - self._instance.data["outputFormat"], - render_prefix, - self._instance.data["outputType"], - self._instance.data["leadingZeros"], - self._instance.data["outputStartFrame"] - ) - - all_write_nodes = self._instance.context.data["all_write_nodes"] - disable_nodes = [] - for node in all_write_nodes: - # disable all other write nodes - if node != self._instance.data["setMembers"][0]: - disable_nodes.append("node.setEnable('{}', false)" - .format(node)) - harmony_plugin_info.PreRenderInlineScript = ';'.join(disable_nodes) - - return harmony_plugin_info.serialize() diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_houdini_cache_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_houdini_cache_deadline.py deleted file mode 100644 index ac9ad570c3..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_houdini_cache_deadline.py +++ /dev/null @@ -1,181 +0,0 @@ -import os -import getpass -from datetime import datetime - -import attr -import pyblish.api -from ayon_core.lib import ( - TextDef, - NumberDef, - is_in_tests, -) -from ayon_core.pipeline import ( - AYONPyblishPluginMixin -) -from ayon_deadline import abstract_submit_deadline -from ayon_deadline.abstract_submit_deadline import DeadlineJobInfo - - -@attr.s -class HoudiniPluginInfo(object): - Build = attr.ib(default=None) - IgnoreInputs = attr.ib(default=True) - ScriptJob = attr.ib(default=True) - SceneFile = attr.ib(default=None) # Input - SaveFile = attr.ib(default=True) - ScriptFilename = attr.ib(default=None) - OutputDriver = attr.ib(default=None) - Version = attr.ib(default=None) # Mandatory for Deadline - ProjectPath = attr.ib(default=None) - - -class HoudiniCacheSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, # noqa - AYONPyblishPluginMixin): - """Submit Houdini scene to perform a local publish in Deadline. - - Publishing in Deadline can be helpful for scenes that publish very slow. - This way it can process in the background on another machine without the - Artist having to wait for the publish to finish on their local machine. - """ - - label = "Submit Scene to Deadline" - order = pyblish.api.IntegratorOrder - hosts = ["houdini"] - families = ["publish.hou"] - targets = ["local"] - settings_category = "deadline" - - priority = 50 - chunk_size = 999999 - group = None - jobInfo = {} - pluginInfo = {} - - - def get_job_info(self): - job_info = DeadlineJobInfo(Plugin="Houdini") - - job_info.update(self.jobInfo) - instance = self._instance - context = instance.context - assert all( - result["success"] for result in context.data["results"] - ), "Errors found, aborting integration.." - - project_name = instance.context.data["projectName"] - filepath = context.data["currentFile"] - scenename = os.path.basename(filepath) - job_name = "{scene} - {instance} [PUBLISH]".format( - scene=scenename, instance=instance.name) - batch_name = "{code} - {scene}".format(code=project_name, - scene=scenename) - if is_in_tests(): - batch_name += datetime.now().strftime("%d%m%Y%H%M%S") - - job_info.Name = job_name - job_info.BatchName = batch_name - job_info.Plugin = instance.data["plugin"] - job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) - rop_node = self.get_rop_node(instance) - if rop_node.type().name() != "alembic": - frames = "{start}-{end}x{step}".format( - start=int(instance.data["frameStart"]), - end=int(instance.data["frameEnd"]), - step=int(instance.data["byFrameStep"]), - ) - - job_info.Frames = frames - - job_info.Pool = instance.data.get("primaryPool") - job_info.SecondaryPool = instance.data.get("secondaryPool") - - attr_values = self.get_attr_values_from_data(instance.data) - - job_info.ChunkSize = instance.data.get("chunk_size", self.chunk_size) - job_info.Comment = context.data.get("comment") - job_info.Priority = attr_values.get("priority", self.priority) - job_info.Group = attr_values.get("group", self.group) - - keys = [ - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "OPENPYPE_SG_USER", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_LOG_NO_COLORS", - ] - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - - for key in keys: - value = environment.get(key) - if not value: - continue - job_info.EnvironmentKeyValue[key] = value - # to recognize render jobs - job_info.add_render_job_env_var() - - return job_info - - def get_plugin_info(self): - # Not all hosts can import this module. - import hou - - instance = self._instance - version = hou.applicationVersionString() - version = ".".join(version.split(".")[:2]) - rop = self.get_rop_node(instance) - plugin_info = HoudiniPluginInfo( - Build=None, - IgnoreInputs=True, - ScriptJob=True, - SceneFile=self.scene_path, - SaveFile=True, - OutputDriver=rop.path(), - Version=version, - ProjectPath=os.path.dirname(self.scene_path) - ) - - plugin_payload = attr.asdict(plugin_info) - - return plugin_payload - - def process(self, instance): - super(HoudiniCacheSubmitDeadline, self).process(instance) - output_dir = os.path.dirname(instance.data["files"][0]) - instance.data["outputDir"] = output_dir - instance.data["toBeRenderedOn"] = "deadline" - - def get_rop_node(self, instance): - # Not all hosts can import this module. - import hou - - rop = instance.data.get("instance_node") - rop_node = hou.node(rop) - - return rop_node - - @classmethod - def get_attribute_defs(cls): - defs = super(HoudiniCacheSubmitDeadline, cls).get_attribute_defs() - defs.extend([ - NumberDef("priority", - minimum=1, - maximum=250, - decimals=0, - default=cls.priority, - label="Priority"), - TextDef("group", - default=cls.group, - label="Group Name"), - ]) - - return defs diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_houdini_render_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_houdini_render_deadline.py deleted file mode 100644 index 7956108e77..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_houdini_render_deadline.py +++ /dev/null @@ -1,403 +0,0 @@ -import os -import attr -import getpass -from datetime import datetime - -import pyblish.api - -from ayon_core.pipeline import AYONPyblishPluginMixin -from ayon_core.lib import ( - is_in_tests, - TextDef, - NumberDef -) -from ayon_deadline import abstract_submit_deadline -from ayon_deadline.abstract_submit_deadline import DeadlineJobInfo - - -@attr.s -class DeadlinePluginInfo(): - SceneFile = attr.ib(default=None) - OutputDriver = attr.ib(default=None) - Version = attr.ib(default=None) - IgnoreInputs = attr.ib(default=True) - - -@attr.s -class ArnoldRenderDeadlinePluginInfo(): - InputFile = attr.ib(default=None) - Verbose = attr.ib(default=4) - - -@attr.s -class MantraRenderDeadlinePluginInfo(): - SceneFile = attr.ib(default=None) - Version = attr.ib(default=None) - - -@attr.s -class VrayRenderPluginInfo(): - InputFilename = attr.ib(default=None) - SeparateFilesPerFrame = attr.ib(default=True) - - -@attr.s -class RedshiftRenderPluginInfo(): - SceneFile = attr.ib(default=None) - # Use "1" as the default Redshift version just because it - # default fallback version in Deadline's Redshift plugin - # if no version was specified - Version = attr.ib(default="1") - - -@attr.s -class HuskStandalonePluginInfo(): - """Requires Deadline Husk Standalone Plugin. - See Deadline Plug-in: - https://github.com/BigRoy/HuskStandaloneSubmitter - Also see Husk options here: - https://www.sidefx.com/docs/houdini/ref/utils/husk.html - """ - SceneFile = attr.ib() - # TODO: Below parameters are only supported by custom version of the plugin - Renderer = attr.ib(default=None) - RenderSettings = attr.ib(default="/Render/rendersettings") - Purpose = attr.ib(default="geometry,render") - Complexity = attr.ib(default="veryhigh") - Snapshot = attr.ib(default=-1) - LogLevel = attr.ib(default="2") - PreRender = attr.ib(default="") - PreFrame = attr.ib(default="") - PostFrame = attr.ib(default="") - PostRender = attr.ib(default="") - RestartDelegate = attr.ib(default="") - Version = attr.ib(default="") - - -class HoudiniSubmitDeadline( - abstract_submit_deadline.AbstractSubmitDeadline, - AYONPyblishPluginMixin -): - """Submit Render ROPs to Deadline. - - Renders are submitted to a Deadline Web Service as - supplied via the environment variable AVALON_DEADLINE. - - Target "local": - Even though this does *not* render locally this is seen as - a 'local' submission as it is the regular way of submitting - a Houdini render locally. - - """ - - label = "Submit Render to Deadline" - order = pyblish.api.IntegratorOrder - hosts = ["houdini"] - families = ["redshift_rop", - "arnold_rop", - "mantra_rop", - "karma_rop", - "vray_rop"] - targets = ["local"] - settings_category = "deadline" - use_published = True - - # presets - export_priority = 50 - export_chunk_size = 10 - export_group = "" - priority = 50 - chunk_size = 1 - group = "" - - @classmethod - def get_attribute_defs(cls): - return [ - NumberDef( - "priority", - label="Priority", - default=cls.priority, - decimals=0 - ), - NumberDef( - "chunk", - label="Frames Per Task", - default=cls.chunk_size, - decimals=0, - minimum=1, - maximum=1000 - ), - TextDef( - "group", - default=cls.group, - label="Group Name" - ), - NumberDef( - "export_priority", - label="Export Priority", - default=cls.export_priority, - decimals=0 - ), - NumberDef( - "export_chunk", - label="Export Frames Per Task", - default=cls.export_chunk_size, - decimals=0, - minimum=1, - maximum=1000 - ), - TextDef( - "export_group", - default=cls.export_group, - label="Export Group Name" - ), - ] - - def get_job_info(self, dependency_job_ids=None): - - instance = self._instance - context = instance.context - - attribute_values = self.get_attr_values_from_data(instance.data) - - # Whether Deadline render submission is being split in two - # (extract + render) - split_render_job = instance.data.get("splitRender") - - # If there's some dependency job ids we can assume this is a render job - # and not an export job - is_export_job = True - if dependency_job_ids: - is_export_job = False - - job_type = "[RENDER]" - if split_render_job and not is_export_job: - product_type = instance.data["productType"] - plugin = { - "usdrender": "HuskStandalone", - }.get(product_type) - if not plugin: - # Convert from product type to Deadline plugin name - # i.e., arnold_rop -> Arnold - plugin = product_type.replace("_rop", "").capitalize() - else: - plugin = "Houdini" - if split_render_job: - job_type = "[EXPORT IFD]" - - job_info = DeadlineJobInfo(Plugin=plugin) - - filepath = context.data["currentFile"] - filename = os.path.basename(filepath) - job_info.Name = "{} - {} {}".format(filename, instance.name, job_type) - job_info.BatchName = filename - - job_info.UserName = context.data.get( - "deadlineUser", getpass.getuser()) - - if is_in_tests(): - job_info.BatchName += datetime.now().strftime("%d%m%Y%H%M%S") - - # Deadline requires integers in frame range - start = instance.data["frameStartHandle"] - end = instance.data["frameEndHandle"] - frames = "{start}-{end}x{step}".format( - start=int(start), - end=int(end), - step=int(instance.data["byFrameStep"]), - ) - job_info.Frames = frames - - # Make sure we make job frame dependent so render tasks pick up a soon - # as export tasks are done - if split_render_job and not is_export_job: - job_info.IsFrameDependent = bool(instance.data.get( - "splitRenderFrameDependent", True)) - - job_info.Pool = instance.data.get("primaryPool") - job_info.SecondaryPool = instance.data.get("secondaryPool") - - if split_render_job and is_export_job: - job_info.Priority = attribute_values.get( - "export_priority", self.export_priority - ) - job_info.ChunkSize = attribute_values.get( - "export_chunk", self.export_chunk_size - ) - job_info.Group = self.export_group - else: - job_info.Priority = attribute_values.get( - "priority", self.priority - ) - job_info.ChunkSize = attribute_values.get( - "chunk", self.chunk_size - ) - job_info.Group = self.group - - # Apply render globals, like e.g. data from collect machine list - render_globals = instance.data.get("renderGlobals", {}) - if render_globals: - self.log.debug("Applying 'renderGlobals' to job info: %s", - render_globals) - job_info.update(render_globals) - - job_info.Comment = context.data.get("comment") - - keys = [ - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "OPENPYPE_SG_USER", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_LOG_NO_COLORS", - ] - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - - for key in keys: - value = environment.get(key) - if value: - job_info.EnvironmentKeyValue[key] = value - - # to recognize render jobs - job_info.add_render_job_env_var() - - for i, filepath in enumerate(instance.data["files"]): - dirname = os.path.dirname(filepath) - fname = os.path.basename(filepath) - job_info.OutputDirectory += dirname.replace("\\", "/") - job_info.OutputFilename += fname - - # Add dependencies if given - if dependency_job_ids: - job_info.JobDependencies = ",".join(dependency_job_ids) - - return job_info - - def get_plugin_info(self, job_type=None): - # Not all hosts can import this module. - import hou - - instance = self._instance - context = instance.context - - hou_major_minor = hou.applicationVersionString().rsplit(".", 1)[0] - - # Output driver to render - if job_type == "render": - product_type = instance.data.get("productType") - if product_type == "arnold_rop": - plugin_info = ArnoldRenderDeadlinePluginInfo( - InputFile=instance.data["ifdFile"] - ) - elif product_type == "mantra_rop": - plugin_info = MantraRenderDeadlinePluginInfo( - SceneFile=instance.data["ifdFile"], - Version=hou_major_minor, - ) - elif product_type == "vray_rop": - plugin_info = VrayRenderPluginInfo( - InputFilename=instance.data["ifdFile"], - ) - elif product_type == "redshift_rop": - plugin_info = RedshiftRenderPluginInfo( - SceneFile=instance.data["ifdFile"] - ) - # Note: To use different versions of Redshift on Deadline - # set the `REDSHIFT_VERSION` env variable in the Tools - # settings in the AYON Application plugin. You will also - # need to set that version in `Redshift.param` file - # of the Redshift Deadline plugin: - # [Redshift_Executable_*] - # where * is the version number. - if os.getenv("REDSHIFT_VERSION"): - plugin_info.Version = os.getenv("REDSHIFT_VERSION") - else: - self.log.warning(( - "REDSHIFT_VERSION env variable is not set" - " - using version configured in Deadline" - )) - - elif product_type == "usdrender": - plugin_info = self._get_husk_standalone_plugin_info( - instance, hou_major_minor) - - else: - self.log.error( - "Product type '%s' not supported yet to split render job", - product_type - ) - return - else: - driver = hou.node(instance.data["instance_node"]) - plugin_info = DeadlinePluginInfo( - SceneFile=context.data["currentFile"], - OutputDriver=driver.path(), - Version=hou_major_minor, - IgnoreInputs=True - ) - - return attr.asdict(plugin_info) - - def process(self, instance): - if not instance.data["farm"]: - self.log.debug("Render on farm is disabled. " - "Skipping deadline submission.") - return - - super(HoudiniSubmitDeadline, self).process(instance) - - # TODO: Avoid the need for this logic here, needed for submit publish - # Store output dir for unified publisher (filesequence) - output_dir = os.path.dirname(instance.data["files"][0]) - instance.data["outputDir"] = output_dir - - def _get_husk_standalone_plugin_info(self, instance, hou_major_minor): - # Not all hosts can import this module. - import hou - - # Supply additional parameters from the USD Render ROP - # to the Husk Standalone Render Plug-in - rop_node = hou.node(instance.data["instance_node"]) - snapshot_interval = -1 - if rop_node.evalParm("dosnapshot"): - snapshot_interval = rop_node.evalParm("snapshotinterval") - - restart_delegate = 0 - if rop_node.evalParm("husk_restartdelegate"): - restart_delegate = rop_node.evalParm("husk_restartdelegateframes") - - rendersettings = ( - rop_node.evalParm("rendersettings") - or "/Render/rendersettings" - ) - return HuskStandalonePluginInfo( - SceneFile=instance.data["ifdFile"], - Renderer=rop_node.evalParm("renderer"), - RenderSettings=rendersettings, - Purpose=rop_node.evalParm("husk_purpose"), - Complexity=rop_node.evalParm("husk_complexity"), - Snapshot=snapshot_interval, - PreRender=rop_node.evalParm("husk_prerender"), - PreFrame=rop_node.evalParm("husk_preframe"), - PostFrame=rop_node.evalParm("husk_postframe"), - PostRender=rop_node.evalParm("husk_postrender"), - RestartDelegate=restart_delegate, - Version=hou_major_minor - ) - - -class HoudiniSubmitDeadlineUsdRender(HoudiniSubmitDeadline): - # Do not use published workfile paths for USD Render ROP because the - # Export Job doesn't seem to occur using the published path either, so - # output paths then do not match the actual rendered paths - use_published = False - families = ["usdrender"] diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_max_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_max_deadline.py deleted file mode 100644 index 6a369eb001..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_max_deadline.py +++ /dev/null @@ -1,431 +0,0 @@ -import os -import getpass -import copy -import attr - -from ayon_core.lib import ( - TextDef, - BoolDef, - NumberDef, -) -from ayon_core.pipeline import ( - AYONPyblishPluginMixin -) -from ayon_core.pipeline.publish.lib import ( - replace_with_published_scene_path -) -from ayon_core.pipeline.publish import KnownPublishError -from ayon_max.api.lib import ( - get_current_renderer, - get_multipass_setting -) -from ayon_max.api.lib_rendersettings import RenderSettings -from ayon_deadline import abstract_submit_deadline -from ayon_deadline.abstract_submit_deadline import DeadlineJobInfo - - -@attr.s -class MaxPluginInfo(object): - SceneFile = attr.ib(default=None) # Input - Version = attr.ib(default=None) # Mandatory for Deadline - SaveFile = attr.ib(default=True) - IgnoreInputs = attr.ib(default=True) - - -class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, - AYONPyblishPluginMixin): - - label = "Submit Render to Deadline" - hosts = ["max"] - families = ["maxrender"] - targets = ["local"] - settings_category = "deadline" - - use_published = True - priority = 50 - chunk_size = 1 - jobInfo = {} - pluginInfo = {} - group = None - - @classmethod - def apply_settings(cls, project_settings): - settings = project_settings["deadline"]["publish"]["MaxSubmitDeadline"] # noqa - - # Take some defaults from settings - cls.use_published = settings.get("use_published", - cls.use_published) - cls.priority = settings.get("priority", - cls.priority) - cls.chuck_size = settings.get("chunk_size", cls.chunk_size) - cls.group = settings.get("group", cls.group) - # TODO: multiple camera instance, separate job infos - def get_job_info(self): - job_info = DeadlineJobInfo(Plugin="3dsmax") - - # todo: test whether this works for existing production cases - # where custom jobInfo was stored in the project settings - job_info.update(self.jobInfo) - - instance = self._instance - context = instance.context - # Always use the original work file name for the Job name even when - # rendering is done from the published Work File. The original work - # file name is clearer because it can also have subversion strings, - # etc. which are stripped for the published file. - - src_filepath = context.data["currentFile"] - src_filename = os.path.basename(src_filepath) - job_info.Name = "%s - %s" % (src_filename, instance.name) - job_info.BatchName = src_filename - job_info.Plugin = instance.data["plugin"] - job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) - job_info.EnableAutoTimeout = True - # Deadline requires integers in frame range - frames = "{start}-{end}".format( - start=int(instance.data["frameStart"]), - end=int(instance.data["frameEnd"]) - ) - job_info.Frames = frames - - job_info.Pool = instance.data.get("primaryPool") - job_info.SecondaryPool = instance.data.get("secondaryPool") - - attr_values = self.get_attr_values_from_data(instance.data) - - job_info.ChunkSize = attr_values.get("chunkSize", 1) - job_info.Comment = context.data.get("comment") - job_info.Priority = attr_values.get("priority", self.priority) - job_info.Group = attr_values.get("group", self.group) - - # Add options from RenderGlobals - render_globals = instance.data.get("renderGlobals", {}) - job_info.update(render_globals) - - keys = [ - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "OPENPYPE_SG_USER", - "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_IN_TESTS", - ] - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - - for key in keys: - value = environment.get(key) - if not value: - continue - job_info.EnvironmentKeyValue[key] = value - - # to recognize render jobs - job_info.add_render_job_env_var() - job_info.EnvironmentKeyValue["AYON_LOG_NO_COLORS"] = "1" - - # Add list of expected files to job - # --------------------------------- - if not instance.data.get("multiCamera"): - exp = instance.data.get("expectedFiles") - for filepath in self._iter_expected_files(exp): - job_info.OutputDirectory += os.path.dirname(filepath) - job_info.OutputFilename += os.path.basename(filepath) - - return job_info - - def get_plugin_info(self): - instance = self._instance - - plugin_info = MaxPluginInfo( - SceneFile=self.scene_path, - Version=instance.data["maxversion"], - SaveFile=True, - IgnoreInputs=True - ) - - plugin_payload = attr.asdict(plugin_info) - - # Patching with pluginInfo from settings - for key, value in self.pluginInfo.items(): - plugin_payload[key] = value - - return plugin_payload - - def process_submission(self): - - instance = self._instance - filepath = instance.context.data["currentFile"] - - files = instance.data["expectedFiles"] - if not files: - raise KnownPublishError("No Render Elements found!") - first_file = next(self._iter_expected_files(files)) - output_dir = os.path.dirname(first_file) - instance.data["outputDir"] = output_dir - - filename = os.path.basename(filepath) - - payload_data = { - "filename": filename, - "dirname": output_dir - } - - self.log.debug("Submitting 3dsMax render..") - project_settings = instance.context.data["project_settings"] - auth = self._instance.data["deadline"]["auth"] - verify = self._instance.data["deadline"]["verify"] - if instance.data.get("multiCamera"): - self.log.debug("Submitting jobs for multiple cameras..") - payload = self._use_published_name_for_multiples( - payload_data, project_settings) - job_infos, plugin_infos = payload - for job_info, plugin_info in zip(job_infos, plugin_infos): - self.submit( - self.assemble_payload(job_info, plugin_info), - auth=auth, - verify=verify - ) - else: - payload = self._use_published_name(payload_data, project_settings) - job_info, plugin_info = payload - self.submit( - self.assemble_payload(job_info, plugin_info), - auth=auth, - verify=verify - ) - - def _use_published_name(self, data, project_settings): - # Not all hosts can import these modules. - from ayon_max.api.lib import ( - get_current_renderer, - get_multipass_setting - ) - from ayon_max.api.lib_rendersettings import RenderSettings - - instance = self._instance - job_info = copy.deepcopy(self.job_info) - plugin_info = copy.deepcopy(self.plugin_info) - plugin_data = {} - - multipass = get_multipass_setting(project_settings) - if multipass: - plugin_data["DisableMultipass"] = 0 - else: - plugin_data["DisableMultipass"] = 1 - - files = instance.data.get("expectedFiles") - if not files: - raise KnownPublishError("No render elements found") - first_file = next(self._iter_expected_files(files)) - old_output_dir = os.path.dirname(first_file) - output_beauty = RenderSettings().get_render_output(instance.name, - old_output_dir) - rgb_bname = os.path.basename(output_beauty) - dir = os.path.dirname(first_file) - beauty_name = f"{dir}/{rgb_bname}" - beauty_name = beauty_name.replace("\\", "/") - plugin_data["RenderOutput"] = beauty_name - # as 3dsmax has version with different languages - plugin_data["Language"] = "ENU" - - renderer_class = get_current_renderer() - - renderer = str(renderer_class).split(":")[0] - if renderer in [ - "ART_Renderer", - "Redshift_Renderer", - "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3", - "Default_Scanline_Renderer", - "Quicksilver_Hardware_Renderer", - ]: - render_elem_list = RenderSettings().get_render_element() - for i, element in enumerate(render_elem_list): - elem_bname = os.path.basename(element) - new_elem = f"{dir}/{elem_bname}" - new_elem = new_elem.replace("/", "\\") - plugin_data["RenderElementOutputFilename%d" % i] = new_elem # noqa - - if renderer == "Redshift_Renderer": - plugin_data["redshift_SeparateAovFiles"] = instance.data.get( - "separateAovFiles") - if instance.data["cameras"]: - camera = instance.data["cameras"][0] - plugin_info["Camera0"] = camera - plugin_info["Camera"] = camera - plugin_info["Camera1"] = camera - self.log.debug("plugin data:{}".format(plugin_data)) - plugin_info.update(plugin_data) - - return job_info, plugin_info - - def get_job_info_through_camera(self, camera): - """Get the job parameters for deadline submission when - multi-camera is enabled. - Args: - infos(dict): a dictionary with job info. - """ - instance = self._instance - context = instance.context - job_info = copy.deepcopy(self.job_info) - exp = instance.data.get("expectedFiles") - - src_filepath = context.data["currentFile"] - src_filename = os.path.basename(src_filepath) - job_info.Name = "%s - %s - %s" % ( - src_filename, instance.name, camera) - for filepath in self._iter_expected_files(exp): - if camera not in filepath: - continue - job_info.OutputDirectory += os.path.dirname(filepath) - job_info.OutputFilename += os.path.basename(filepath) - - return job_info - # set the output filepath with the relative camera - - def get_plugin_info_through_camera(self, camera): - """Get the plugin parameters for deadline submission when - multi-camera is enabled. - Args: - infos(dict): a dictionary with plugin info. - """ - instance = self._instance - # set the target camera - plugin_info = copy.deepcopy(self.plugin_info) - - plugin_data = {} - # set the output filepath with the relative camera - if instance.data.get("multiCamera"): - scene_filepath = instance.context.data["currentFile"] - scene_filename = os.path.basename(scene_filepath) - scene_directory = os.path.dirname(scene_filepath) - current_filename, ext = os.path.splitext(scene_filename) - camera_scene_name = f"{current_filename}_{camera}{ext}" - camera_scene_filepath = os.path.join( - scene_directory, f"_{current_filename}", camera_scene_name) - plugin_data["SceneFile"] = camera_scene_filepath - - files = instance.data.get("expectedFiles") - if not files: - raise KnownPublishError("No render elements found") - first_file = next(self._iter_expected_files(files)) - old_output_dir = os.path.dirname(first_file) - rgb_output = RenderSettings().get_batch_render_output(camera) # noqa - rgb_bname = os.path.basename(rgb_output) - dir = os.path.dirname(first_file) - beauty_name = f"{dir}/{rgb_bname}" - beauty_name = beauty_name.replace("\\", "/") - plugin_info["RenderOutput"] = beauty_name - renderer_class = get_current_renderer() - - renderer = str(renderer_class).split(":")[0] - if renderer in [ - "ART_Renderer", - "Redshift_Renderer", - "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3", - "Default_Scanline_Renderer", - "Quicksilver_Hardware_Renderer", - ]: - render_elem_list = RenderSettings().get_batch_render_elements( - instance.name, old_output_dir, camera - ) - for i, element in enumerate(render_elem_list): - if camera in element: - elem_bname = os.path.basename(element) - new_elem = f"{dir}/{elem_bname}" - new_elem = new_elem.replace("/", "\\") - plugin_info["RenderElementOutputFilename%d" % i] = new_elem # noqa - - if camera: - # set the default camera and target camera - # (weird parameters from max) - plugin_data["Camera"] = camera - plugin_data["Camera1"] = camera - plugin_data["Camera0"] = None - - plugin_info.update(plugin_data) - return plugin_info - - def _use_published_name_for_multiples(self, data, project_settings): - """Process the parameters submission for deadline when - user enables multi-cameras option. - Args: - job_info_list (list): A list of multiple job infos - plugin_info_list (list): A list of multiple plugin infos - """ - job_info_list = [] - plugin_info_list = [] - instance = self._instance - cameras = instance.data.get("cameras", []) - plugin_data = {} - multipass = get_multipass_setting(project_settings) - if multipass: - plugin_data["DisableMultipass"] = 0 - else: - plugin_data["DisableMultipass"] = 1 - for cam in cameras: - job_info = self.get_job_info_through_camera(cam) - plugin_info = self.get_plugin_info_through_camera(cam) - plugin_info.update(plugin_data) - job_info_list.append(job_info) - plugin_info_list.append(plugin_info) - - return job_info_list, plugin_info_list - - def from_published_scene(self, replace_in_path=True): - instance = self._instance - if instance.data["renderer"] == "Redshift_Renderer": - self.log.debug("Using Redshift...published scene wont be used..") - replace_in_path = False - return replace_with_published_scene_path( - instance, replace_in_path) - - @staticmethod - def _iter_expected_files(exp): - if isinstance(exp[0], dict): - for _aov, files in exp[0].items(): - for file in files: - yield file - else: - for file in exp: - yield file - - @classmethod - def get_attribute_defs(cls): - defs = super(MaxSubmitDeadline, cls).get_attribute_defs() - defs.extend([ - BoolDef("use_published", - default=cls.use_published, - label="Use Published Scene"), - - NumberDef("priority", - minimum=1, - maximum=250, - decimals=0, - default=cls.priority, - label="Priority"), - - NumberDef("chunkSize", - minimum=1, - maximum=50, - decimals=0, - default=cls.chunk_size, - label="Frame Per Task"), - - TextDef("group", - default=cls.group, - label="Group Name"), - ]) - - return defs diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_maya_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_maya_deadline.py deleted file mode 100644 index d50b0147d9..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_maya_deadline.py +++ /dev/null @@ -1,935 +0,0 @@ -# -*- coding: utf-8 -*- -"""Submitting render job to Deadline. - -This module is taking care of submitting job from Maya to Deadline. It -creates job and set correct environments. Its behavior is controlled by -``DEADLINE_REST_URL`` environment variable - pointing to Deadline Web Service -and :data:`MayaSubmitDeadline.use_published` property telling Deadline to -use published scene workfile or not. - -If ``vrscene`` or ``assscene`` are detected in families, it will first -submit job to export these files and then dependent job to render them. - -Attributes: - payload_skeleton (dict): Skeleton payload data sent as job to Deadline. - Default values are for ``MayaBatch`` plugin. - -""" - -from __future__ import print_function -import os -import json -import getpass -import copy -import re -import hashlib -from datetime import datetime -import itertools -from collections import OrderedDict - -import attr - -from ayon_core.pipeline import ( - AYONPyblishPluginMixin -) -from ayon_core.lib import ( - BoolDef, - NumberDef, - TextDef, - EnumDef, - is_in_tests, -) -from ayon_maya.api.lib_rendersettings import RenderSettings -from ayon_maya.api.lib import get_attr_in_layer - -from ayon_core.pipeline.farm.tools import iter_expected_files - -from ayon_deadline import abstract_submit_deadline -from ayon_deadline.abstract_submit_deadline import DeadlineJobInfo - - -def _validate_deadline_bool_value(instance, attribute, value): - if not isinstance(value, (str, bool)): - raise TypeError( - "Attribute {} must be str or bool.".format(attribute)) - if value not in {"1", "0", True, False}: - raise ValueError( - ("Value of {} must be one of " - "'0', '1', True, False").format(attribute) - ) - - -@attr.s -class MayaPluginInfo(object): - SceneFile = attr.ib(default=None) # Input - OutputFilePath = attr.ib(default=None) # Output directory and filename - OutputFilePrefix = attr.ib(default=None) - Version = attr.ib(default=None) # Mandatory for Deadline - UsingRenderLayers = attr.ib(default=True) - RenderLayer = attr.ib(default=None) # Render only this layer - Renderer = attr.ib(default=None) - ProjectPath = attr.ib(default=None) # Resolve relative references - # Include all lights flag - RenderSetupIncludeLights = attr.ib( - default="1", validator=_validate_deadline_bool_value) - StrictErrorChecking = attr.ib(default=True) - - -@attr.s -class PythonPluginInfo(object): - ScriptFile = attr.ib() - Version = attr.ib(default="3.6") - Arguments = attr.ib(default=None) - SingleFrameOnly = attr.ib(default=None) - - -@attr.s -class VRayPluginInfo(object): - InputFilename = attr.ib(default=None) # Input - SeparateFilesPerFrame = attr.ib(default=None) - VRayEngine = attr.ib(default="V-Ray") - Width = attr.ib(default=None) - Height = attr.ib(default=None) # Mandatory for Deadline - OutputFilePath = attr.ib(default=True) - OutputFileName = attr.ib(default=None) # Render only this layer - - -@attr.s -class ArnoldPluginInfo(object): - ArnoldFile = attr.ib(default=None) - - -class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, - AYONPyblishPluginMixin): - - label = "Submit Render to Deadline" - hosts = ["maya"] - families = ["renderlayer"] - targets = ["local"] - settings_category = "deadline" - - tile_assembler_plugin = "OpenPypeTileAssembler" - priority = 50 - tile_priority = 50 - limit = [] # limit groups - jobInfo = {} - pluginInfo = {} - group = "none" - strict_error_checking = True - - @classmethod - def apply_settings(cls, project_settings): - settings = project_settings["deadline"]["publish"]["MayaSubmitDeadline"] # noqa - - # Take some defaults from settings - cls.asset_dependencies = settings.get("asset_dependencies", - cls.asset_dependencies) - cls.import_reference = settings.get("import_reference", - cls.import_reference) - cls.use_published = settings.get("use_published", cls.use_published) - cls.priority = settings.get("priority", cls.priority) - cls.tile_priority = settings.get("tile_priority", cls.tile_priority) - cls.limit = settings.get("limit", cls.limit) - cls.group = settings.get("group", cls.group) - cls.strict_error_checking = settings.get("strict_error_checking", - cls.strict_error_checking) - job_info = settings.get("jobInfo") - if job_info: - job_info = json.loads(job_info) - plugin_info = settings.get("pluginInfo") - if plugin_info: - plugin_info = json.loads(plugin_info) - - cls.jobInfo = job_info or cls.jobInfo - cls.pluginInfo = plugin_info or cls.pluginInfo - - def get_job_info(self): - job_info = DeadlineJobInfo(Plugin="MayaBatch") - - # todo: test whether this works for existing production cases - # where custom jobInfo was stored in the project settings - job_info.update(self.jobInfo) - - instance = self._instance - context = instance.context - - # Always use the original work file name for the Job name even when - # rendering is done from the published Work File. The original work - # file name is clearer because it can also have subversion strings, - # etc. which are stripped for the published file. - src_filepath = context.data["currentFile"] - src_filename = os.path.basename(src_filepath) - - if is_in_tests(): - src_filename += datetime.now().strftime("%d%m%Y%H%M%S") - - job_info.Name = "%s - %s" % (src_filename, instance.name) - job_info.BatchName = src_filename - job_info.Plugin = instance.data.get("mayaRenderPlugin", "MayaBatch") - job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) - - # Deadline requires integers in frame range - frames = "{start}-{end}x{step}".format( - start=int(instance.data["frameStartHandle"]), - end=int(instance.data["frameEndHandle"]), - step=int(instance.data["byFrameStep"]), - ) - job_info.Frames = frames - - job_info.Pool = instance.data.get("primaryPool") - job_info.SecondaryPool = instance.data.get("secondaryPool") - job_info.Comment = context.data.get("comment") - job_info.Priority = instance.data.get("priority", self.priority) - - if self.group != "none" and self.group: - job_info.Group = self.group - - if self.limit: - job_info.LimitGroups = ",".join(self.limit) - - attr_values = self.get_attr_values_from_data(instance.data) - render_globals = instance.data.setdefault("renderGlobals", dict()) - machine_list = attr_values.get("machineList", "") - if machine_list: - if attr_values.get("whitelist", True): - machine_list_key = "Whitelist" - else: - machine_list_key = "Blacklist" - render_globals[machine_list_key] = machine_list - - job_info.Priority = attr_values.get("priority") - job_info.ChunkSize = attr_values.get("chunkSize") - - # Add options from RenderGlobals - render_globals = instance.data.get("renderGlobals", {}) - job_info.update(render_globals) - - keys = [ - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "OPENPYPE_SG_USER", - "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_IN_TESTS" - ] - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - - for key in keys: - value = environment.get(key) - if not value: - continue - job_info.EnvironmentKeyValue[key] = value - - # to recognize render jobs - job_info.add_render_job_env_var() - job_info.EnvironmentKeyValue["AYON_LOG_NO_COLORS"] = "1" - - # Adding file dependencies. - if not is_in_tests() and self.asset_dependencies: - dependencies = instance.context.data["fileDependencies"] - for dependency in dependencies: - job_info.AssetDependency += dependency - - # Add list of expected files to job - # --------------------------------- - exp = instance.data.get("expectedFiles") - for filepath in iter_expected_files(exp): - job_info.OutputDirectory += os.path.dirname(filepath) - job_info.OutputFilename += os.path.basename(filepath) - - return job_info - - def get_plugin_info(self): - # Not all hosts can import this module. - from maya import cmds - - instance = self._instance - context = instance.context - - # Set it to default Maya behaviour if it cannot be determined - # from instance (but it should be, by the Collector). - - default_rs_include_lights = ( - instance.context.data['project_settings'] - ['maya'] - ['render_settings'] - ['enable_all_lights'] - ) - - rs_include_lights = instance.data.get( - "renderSetupIncludeLights", default_rs_include_lights) - if rs_include_lights not in {"1", "0", True, False}: - rs_include_lights = default_rs_include_lights - - attr_values = self.get_attr_values_from_data(instance.data) - strict_error_checking = attr_values.get("strict_error_checking", - self.strict_error_checking) - plugin_info = MayaPluginInfo( - SceneFile=self.scene_path, - Version=cmds.about(version=True), - RenderLayer=instance.data['setMembers'], - Renderer=instance.data["renderer"], - RenderSetupIncludeLights=rs_include_lights, # noqa - ProjectPath=context.data["workspaceDir"], - UsingRenderLayers=True, - StrictErrorChecking=strict_error_checking - ) - - plugin_payload = attr.asdict(plugin_info) - - # Patching with pluginInfo from settings - for key, value in self.pluginInfo.items(): - plugin_payload[key] = value - - return plugin_payload - - def process_submission(self): - from maya import cmds - instance = self._instance - - filepath = self.scene_path # publish if `use_publish` else workfile - - # TODO: Avoid the need for this logic here, needed for submit publish - # Store output dir for unified publisher (filesequence) - expected_files = instance.data["expectedFiles"] - first_file = next(iter_expected_files(expected_files)) - output_dir = os.path.dirname(first_file) - instance.data["outputDir"] = output_dir - - # Patch workfile (only when use_published is enabled) - if self.use_published: - self._patch_workfile() - - # Gather needed data ------------------------------------------------ - filename = os.path.basename(filepath) - dirname = os.path.join( - cmds.workspace(query=True, rootDirectory=True), - cmds.workspace(fileRuleEntry="images") - ) - - # Fill in common data to payload ------------------------------------ - # TODO: Replace these with collected data from CollectRender - payload_data = { - "filename": filename, - "dirname": dirname, - } - - # Submit preceding export jobs ------------------------------------- - export_job = None - assert not all(x in instance.data["families"] - for x in ['vrayscene', 'assscene']), ( - "Vray Scene and Ass Scene options are mutually exclusive") - - auth = self._instance.data["deadline"]["auth"] - verify = self._instance.data["deadline"]["verify"] - if "vrayscene" in instance.data["families"]: - self.log.debug("Submitting V-Ray scene render..") - vray_export_payload = self._get_vray_export_payload(payload_data) - export_job = self.submit(vray_export_payload, - auth=auth, - verify=verify) - - payload = self._get_vray_render_payload(payload_data) - - else: - self.log.debug("Submitting MayaBatch render..") - payload = self._get_maya_payload(payload_data) - - # Add export job as dependency -------------------------------------- - if export_job: - job_info, _ = payload - job_info.JobDependencies = export_job - - if instance.data.get("tileRendering"): - # Prepare tiles data - self._tile_render(payload) - else: - # Submit main render job - job_info, plugin_info = payload - self.submit(self.assemble_payload(job_info, plugin_info), - auth=auth, - verify=verify) - - def _tile_render(self, payload): - """Submit as tile render per frame with dependent assembly jobs.""" - - # As collected by super process() - instance = self._instance - - payload_job_info, payload_plugin_info = payload - job_info = copy.deepcopy(payload_job_info) - plugin_info = copy.deepcopy(payload_plugin_info) - - # Force plugin reload for vray cause the region does not get flushed - # between tile renders. - if plugin_info["Renderer"] == "vray": - job_info.ForceReloadPlugin = True - - # if we have sequence of files, we need to create tile job for - # every frame - job_info.TileJob = True - job_info.TileJobTilesInX = instance.data.get("tilesX") - job_info.TileJobTilesInY = instance.data.get("tilesY") - - tiles_count = job_info.TileJobTilesInX * job_info.TileJobTilesInY - - plugin_info["ImageHeight"] = instance.data.get("resolutionHeight") - plugin_info["ImageWidth"] = instance.data.get("resolutionWidth") - plugin_info["RegionRendering"] = True - - R_FRAME_NUMBER = re.compile( - r".+\.(?P[0-9]+)\..+") # noqa: N806, E501 - REPL_FRAME_NUMBER = re.compile( - r"(.+\.)([0-9]+)(\..+)") # noqa: N806, E501 - - exp = instance.data["expectedFiles"] - if isinstance(exp[0], dict): - # we have aovs and we need to iterate over them - # get files from `beauty` - files = exp[0].get("beauty") - # assembly files are used for assembly jobs as we need to put - # together all AOVs - assembly_files = list( - itertools.chain.from_iterable( - [f for _, f in exp[0].items()])) - if not files: - # if beauty doesn't exist, use first aov we found - files = exp[0].get(list(exp[0].keys())[0]) - else: - files = exp - assembly_files = files - - auth = instance.data["deadline"]["auth"] - verify = instance.data["deadline"]["verify"] - - # Define frame tile jobs - frame_file_hash = {} - frame_payloads = {} - file_index = 1 - for file in files: - frame = re.search(R_FRAME_NUMBER, file).group("frame") - - new_job_info = copy.deepcopy(job_info) - new_job_info.Name += " (Frame {} - {} tiles)".format(frame, - tiles_count) - new_job_info.TileJobFrame = frame - - new_plugin_info = copy.deepcopy(plugin_info) - - # Add tile data into job info and plugin info - tiles_data = _format_tiles( - file, 0, - instance.data.get("tilesX"), - instance.data.get("tilesY"), - instance.data.get("resolutionWidth"), - instance.data.get("resolutionHeight"), - payload_plugin_info["OutputFilePrefix"] - )[0] - - new_job_info.update(tiles_data["JobInfo"]) - new_plugin_info.update(tiles_data["PluginInfo"]) - - self.log.debug("hashing {} - {}".format(file_index, file)) - job_hash = hashlib.sha256( - ("{}_{}".format(file_index, file)).encode("utf-8")) - - file_hash = job_hash.hexdigest() - frame_file_hash[frame] = file_hash - - new_job_info.ExtraInfo[0] = file_hash - new_job_info.ExtraInfo[1] = file - - frame_payloads[frame] = self.assemble_payload( - job_info=new_job_info, - plugin_info=new_plugin_info - ) - file_index += 1 - - self.log.debug( - "Submitting tile job(s) [{}] ...".format(len(frame_payloads))) - - # Submit frame tile jobs - frame_tile_job_id = {} - for frame, tile_job_payload in frame_payloads.items(): - job_id = self.submit( - tile_job_payload, auth, verify) - frame_tile_job_id[frame] = job_id - - # Define assembly payloads - assembly_job_info = copy.deepcopy(job_info) - assembly_job_info.Plugin = self.tile_assembler_plugin - assembly_job_info.Name += " - Tile Assembly Job" - assembly_job_info.Frames = 1 - assembly_job_info.MachineLimit = 1 - - attr_values = self.get_attr_values_from_data(instance.data) - assembly_job_info.Priority = attr_values.get("tile_priority", - self.tile_priority) - assembly_job_info.TileJob = False - - # TODO: This should be a new publisher attribute definition - pool = instance.context.data["project_settings"]["deadline"] - pool = pool["publish"]["ProcessSubmittedJobOnFarm"]["deadline_pool"] - assembly_job_info.Pool = pool or instance.data.get("primaryPool", "") - - assembly_plugin_info = { - "CleanupTiles": 1, - "ErrorOnMissing": True, - "Renderer": self._instance.data["renderer"] - } - - assembly_payloads = [] - output_dir = self.job_info.OutputDirectory[0] - config_files = [] - for file in assembly_files: - frame = re.search(R_FRAME_NUMBER, file).group("frame") - - frame_assembly_job_info = copy.deepcopy(assembly_job_info) - frame_assembly_job_info.Name += " (Frame {})".format(frame) - frame_assembly_job_info.OutputFilename[0] = re.sub( - REPL_FRAME_NUMBER, - "\\1{}\\3".format("#" * len(frame)), file) - - file_hash = frame_file_hash[frame] - tile_job_id = frame_tile_job_id[frame] - - frame_assembly_job_info.ExtraInfo[0] = file_hash - frame_assembly_job_info.ExtraInfo[1] = file - frame_assembly_job_info.JobDependencies = tile_job_id - frame_assembly_job_info.Frames = frame - - # write assembly job config files - config_file = os.path.join( - output_dir, - "{}_config_{}.txt".format( - os.path.splitext(file)[0], - datetime.now().strftime("%Y_%m_%d_%H_%M_%S") - ) - ) - config_files.append(config_file) - try: - if not os.path.isdir(output_dir): - os.makedirs(output_dir) - except OSError: - # directory is not available - self.log.warning("Path is unreachable: " - "`{}`".format(output_dir)) - - with open(config_file, "w") as cf: - print("TileCount={}".format(tiles_count), file=cf) - print("ImageFileName={}".format(file), file=cf) - print("ImageWidth={}".format( - instance.data.get("resolutionWidth")), file=cf) - print("ImageHeight={}".format( - instance.data.get("resolutionHeight")), file=cf) - - reversed_y = False - if plugin_info["Renderer"] == "arnold": - reversed_y = True - - with open(config_file, "a") as cf: - # Need to reverse the order of the y tiles, because image - # coordinates are calculated from bottom left corner. - tiles = _format_tiles( - file, 0, - instance.data.get("tilesX"), - instance.data.get("tilesY"), - instance.data.get("resolutionWidth"), - instance.data.get("resolutionHeight"), - payload_plugin_info["OutputFilePrefix"], - reversed_y=reversed_y - )[1] - for k, v in sorted(tiles.items()): - print("{}={}".format(k, v), file=cf) - - assembly_payloads.append( - self.assemble_payload( - job_info=frame_assembly_job_info, - plugin_info=assembly_plugin_info.copy(), - # This would fail if the client machine and webserice are - # using different storage paths. - aux_files=[config_file] - ) - ) - - # Submit assembly jobs - assembly_job_ids = [] - num_assemblies = len(assembly_payloads) - for i, payload in enumerate(assembly_payloads): - self.log.debug( - "submitting assembly job {} of {}".format(i + 1, - num_assemblies) - ) - assembly_job_id = self.submit( - payload, - auth=auth, - verify=verify - ) - assembly_job_ids.append(assembly_job_id) - - instance.data["assemblySubmissionJobs"] = assembly_job_ids - - # Remove config files to avoid confusion about where data is coming - # from in Deadline. - for config_file in config_files: - os.remove(config_file) - - def _get_maya_payload(self, data): - - job_info = copy.deepcopy(self.job_info) - - if not is_in_tests() and self.asset_dependencies: - # Asset dependency to wait for at least the scene file to sync. - job_info.AssetDependency += self.scene_path - - # Get layer prefix - renderlayer = self._instance.data["setMembers"] - renderer = self._instance.data["renderer"] - layer_prefix_attr = RenderSettings.get_image_prefix_attr(renderer) - layer_prefix = get_attr_in_layer(layer_prefix_attr, layer=renderlayer) - - plugin_info = copy.deepcopy(self.plugin_info) - plugin_info.update({ - # Output directory and filename - "OutputFilePath": data["dirname"].replace("\\", "/"), - "OutputFilePrefix": layer_prefix, - }) - - # This hack is here because of how Deadline handles Renderman version. - # it considers everything with `renderman` set as version older than - # Renderman 22, and so if we are using renderman > 21 we need to set - # renderer string on the job to `renderman22`. We will have to change - # this when Deadline releases new version handling this. - renderer = self._instance.data["renderer"] - if renderer == "renderman": - try: - from rfm2.config import cfg # noqa - except ImportError: - raise Exception("Cannot determine renderman version") - - rman_version = cfg().build_info.version() # type: str - if int(rman_version.split(".")[0]) > 22: - renderer = "renderman22" - - plugin_info["Renderer"] = renderer - - # this is needed because renderman plugin in Deadline - # handles directory and file prefixes separately - plugin_info["OutputFilePath"] = job_info.OutputDirectory[0] - - return job_info, plugin_info - - def _get_vray_export_payload(self, data): - - job_info = copy.deepcopy(self.job_info) - job_info.Name = self._job_info_label("Export") - - # Get V-Ray settings info to compute output path - vray_scene = self.format_vray_output_filename() - - plugin_info = { - "Renderer": "vray", - "SkipExistingFrames": True, - "UseLegacyRenderLayers": True, - "OutputFilePath": os.path.dirname(vray_scene) - } - - return job_info, attr.asdict(plugin_info) - - def _get_vray_render_payload(self, data): - - # Job Info - job_info = copy.deepcopy(self.job_info) - job_info.Name = self._job_info_label("Render") - job_info.Plugin = "Vray" - job_info.OverrideTaskExtraInfoNames = False - - # Plugin Info - plugin_info = VRayPluginInfo( - InputFilename=self.format_vray_output_filename(), - SeparateFilesPerFrame=False, - VRayEngine="V-Ray", - Width=self._instance.data["resolutionWidth"], - Height=self._instance.data["resolutionHeight"], - OutputFilePath=job_info.OutputDirectory[0], - OutputFileName=job_info.OutputFilename[0] - ) - - return job_info, attr.asdict(plugin_info) - - def _get_arnold_render_payload(self, data): - # Job Info - job_info = copy.deepcopy(self.job_info) - job_info.Name = self._job_info_label("Render") - job_info.Plugin = "Arnold" - job_info.OverrideTaskExtraInfoNames = False - - # Plugin Info - ass_file, _ = os.path.splitext(data["output_filename_0"]) - ass_filepath = ass_file + ".ass" - - plugin_info = ArnoldPluginInfo( - ArnoldFile=ass_filepath - ) - - return job_info, attr.asdict(plugin_info) - - def format_vray_output_filename(self): - """Format the expected output file of the Export job. - - Example: - /_/ - "shot010_v006/shot010_v006_CHARS/CHARS_0001.vrscene" - Returns: - str - - """ - from maya import cmds - # "vrayscene//_/" - vray_settings = cmds.ls(type="VRaySettingsNode") - node = vray_settings[0] - template = cmds.getAttr("{}.vrscene_filename".format(node)) - scene, _ = os.path.splitext(self.scene_path) - - def smart_replace(string, key_values): - new_string = string - for key, value in key_values.items(): - new_string = new_string.replace(key, value) - return new_string - - # Get workfile scene path without extension to format vrscene_filename - scene_filename = os.path.basename(self.scene_path) - scene_filename_no_ext, _ = os.path.splitext(scene_filename) - - layer = self._instance.data['setMembers'] - - # Reformat without tokens - output_path = smart_replace( - template, - {"": scene_filename_no_ext, - "": layer}) - - start_frame = int(self._instance.data["frameStartHandle"]) - workspace = self._instance.context.data["workspace"] - filename_zero = "{}_{:04d}.vrscene".format(output_path, start_frame) - filepath_zero = os.path.join(workspace, filename_zero) - - return filepath_zero.replace("\\", "/") - - def _patch_workfile(self): - """Patch Maya scene. - - This will take list of patches (lines to add) and apply them to - *published* Maya scene file (that is used later for rendering). - - Patches are dict with following structure:: - { - "name": "Name of patch", - "regex": "regex of line before patch", - "line": "line to insert" - } - - """ - project_settings = self._instance.context.data["project_settings"] - patches = ( - project_settings.get( - "deadline", {}).get( - "publish", {}).get( - "MayaSubmitDeadline", {}).get( - "scene_patches", {}) - ) - if not patches: - return - - if not os.path.splitext(self.scene_path)[1].lower() != ".ma": - self.log.debug("Skipping workfile patch since workfile is not " - ".ma file") - return - - compiled_regex = [re.compile(p["regex"]) for p in patches] - with open(self.scene_path, "r+") as pf: - scene_data = pf.readlines() - for ln, line in enumerate(scene_data): - for i, r in enumerate(compiled_regex): - if re.match(r, line): - scene_data.insert(ln + 1, patches[i]["line"]) - pf.seek(0) - pf.writelines(scene_data) - pf.truncate() - self.log.info("Applied {} patch to scene.".format( - patches[i]["name"] - )) - - def _job_info_label(self, label): - return "{label} {job.Name} [{start}-{end}]".format( - label=label, - job=self.job_info, - start=int(self._instance.data["frameStartHandle"]), - end=int(self._instance.data["frameEndHandle"]), - ) - - @classmethod - def get_attribute_defs(cls): - defs = super(MayaSubmitDeadline, cls).get_attribute_defs() - - defs.extend([ - NumberDef("priority", - label="Priority", - default=cls.default_priority, - decimals=0), - NumberDef("chunkSize", - label="Frames Per Task", - default=1, - decimals=0, - minimum=1, - maximum=1000), - TextDef("machineList", - label="Machine List", - default="", - placeholder="machine1,machine2"), - EnumDef("whitelist", - label="Machine List (Allow/Deny)", - items={ - True: "Allow List", - False: "Deny List", - }, - default=False), - NumberDef("tile_priority", - label="Tile Assembler Priority", - decimals=0, - default=cls.tile_priority), - BoolDef("strict_error_checking", - label="Strict Error Checking", - default=cls.strict_error_checking), - - ]) - - return defs - -def _format_tiles( - filename, - index, - tiles_x, - tiles_y, - width, - height, - prefix, - reversed_y=False -): - """Generate tile entries for Deadline tile job. - - Returns two dictionaries - one that can be directly used in Deadline - job, second that can be used for Deadline Assembly job configuration - file. - - This will format tile names: - - Example:: - { - "OutputFilename0Tile0": "_tile_1x1_4x4_Main_beauty.1001.exr", - "OutputFilename0Tile1": "_tile_2x1_4x4_Main_beauty.1001.exr" - } - - And add tile prefixes like: - - Example:: - Image prefix is: - `//_` - - Result for tile 0 for 4x4 will be: - `//_tile_1x1_4x4__` - - Calculating coordinates is tricky as in Job they are defined as top, - left, bottom, right with zero being in top-left corner. But Assembler - configuration file takes tile coordinates as X, Y, Width and Height and - zero is bottom left corner. - - Args: - filename (str): Filename to process as tiles. - index (int): Index of that file if it is sequence. - tiles_x (int): Number of tiles in X. - tiles_y (int): Number of tiles in Y. - width (int): Width resolution of final image. - height (int): Height resolution of final image. - prefix (str): Image prefix. - reversed_y (bool): Reverses the order of the y tiles. - - Returns: - (dict, dict): Tuple of two dictionaries - first can be used to - extend JobInfo, second has tiles x, y, width and height - used for assembler configuration. - - """ - # Math used requires integers for correct output - as such - # we ensure our inputs are correct. - assert isinstance(tiles_x, int), "tiles_x must be an integer" - assert isinstance(tiles_y, int), "tiles_y must be an integer" - assert isinstance(width, int), "width must be an integer" - assert isinstance(height, int), "height must be an integer" - - out = {"JobInfo": {}, "PluginInfo": {}} - cfg = OrderedDict() - w_space = width // tiles_x - h_space = height // tiles_y - - cfg["TilesCropped"] = "False" - - tile = 0 - range_y = range(1, tiles_y + 1) - reversed_y_range = list(reversed(range_y)) - for tile_x in range(1, tiles_x + 1): - for i, tile_y in enumerate(range_y): - tile_y_index = tile_y - if reversed_y: - tile_y_index = reversed_y_range[i] - - tile_prefix = "_tile_{}x{}_{}x{}_".format( - tile_x, tile_y_index, tiles_x, tiles_y - ) - - new_filename = "{}/{}{}".format( - os.path.dirname(filename), - tile_prefix, - os.path.basename(filename) - ) - - top = height - (tile_y * h_space) - bottom = height - ((tile_y - 1) * h_space) - 1 - left = (tile_x - 1) * w_space - right = (tile_x * w_space) - 1 - - # Job info - key = "OutputFilename{}".format(index) - out["JobInfo"][key] = new_filename - - # Plugin Info - key = "RegionPrefix{}".format(str(tile)) - out["PluginInfo"][key] = "/{}".format( - tile_prefix - ).join(prefix.rsplit("/", 1)) - out["PluginInfo"]["RegionTop{}".format(tile)] = top - out["PluginInfo"]["RegionBottom{}".format(tile)] = bottom - out["PluginInfo"]["RegionLeft{}".format(tile)] = left - out["PluginInfo"]["RegionRight{}".format(tile)] = right - - # Tile config - cfg["Tile{}FileName".format(tile)] = new_filename - cfg["Tile{}X".format(tile)] = left - cfg["Tile{}Y".format(tile)] = top - cfg["Tile{}Width".format(tile)] = w_space - cfg["Tile{}Height".format(tile)] = h_space - - tile += 1 - - return out, cfg diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_nuke_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_nuke_deadline.py deleted file mode 100644 index 7ead5142cf..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_nuke_deadline.py +++ /dev/null @@ -1,558 +0,0 @@ -import os -import re -import json -import getpass -from datetime import datetime - -import pyblish.api - -from ayon_core.pipeline.publish import ( - AYONPyblishPluginMixin -) -from ayon_core.lib import ( - is_in_tests, - BoolDef, - NumberDef -) -from ayon_deadline.abstract_submit_deadline import requests_post - - -class NukeSubmitDeadline(pyblish.api.InstancePlugin, - AYONPyblishPluginMixin): - """Submit write to Deadline - - Renders are submitted to a Deadline Web Service as - supplied via settings key "DEADLINE_REST_URL". - - """ - - label = "Submit Nuke to Deadline" - order = pyblish.api.IntegratorOrder + 0.1 - hosts = ["nuke"] - families = ["render", "prerender"] - optional = True - targets = ["local"] - settings_category = "deadline" - - # presets - priority = 50 - chunk_size = 1 - concurrent_tasks = 1 - group = "" - department = "" - limit_groups = [] - use_gpu = False - env_allowed_keys = [] - env_search_replace_values = [] - workfile_dependency = True - use_published_workfile = True - - @classmethod - def get_attribute_defs(cls): - return [ - NumberDef( - "priority", - label="Priority", - default=cls.priority, - decimals=0 - ), - NumberDef( - "chunk", - label="Frames Per Task", - default=cls.chunk_size, - decimals=0, - minimum=1, - maximum=1000 - ), - NumberDef( - "concurrency", - label="Concurrency", - default=cls.concurrent_tasks, - decimals=0, - minimum=1, - maximum=10 - ), - BoolDef( - "use_gpu", - default=cls.use_gpu, - label="Use GPU" - ), - BoolDef( - "workfile_dependency", - default=cls.workfile_dependency, - label="Workfile Dependency" - ), - BoolDef( - "use_published_workfile", - default=cls.use_published_workfile, - label="Use Published Workfile" - ) - ] - - def process(self, instance): - if not instance.data.get("farm"): - self.log.debug("Skipping local instance.") - return - instance.data["attributeValues"] = self.get_attr_values_from_data( - instance.data) - - families = instance.data["families"] - - node = instance.data["transientData"]["node"] - context = instance.context - - deadline_url = instance.data["deadline"]["url"] - assert deadline_url, "Requires Deadline Webservice URL" - - self.deadline_url = "{}/api/jobs".format(deadline_url) - self._comment = context.data.get("comment", "") - self._ver = re.search(r"\d+\.\d+", context.data.get("hostVersion")) - self._deadline_user = context.data.get( - "deadlineUser", getpass.getuser()) - submit_frame_start = int(instance.data["frameStartHandle"]) - submit_frame_end = int(instance.data["frameEndHandle"]) - - # get output path - render_path = instance.data['path'] - script_path = context.data["currentFile"] - - use_published_workfile = instance.data["attributeValues"].get( - "use_published_workfile", self.use_published_workfile - ) - if use_published_workfile: - script_path = self._get_published_workfile_path(context) - - # only add main rendering job if target is not frames_farm - r_job_response_json = None - if instance.data["render_target"] != "frames_farm": - r_job_response = self.payload_submit( - instance, - script_path, - render_path, - node.name(), - submit_frame_start, - submit_frame_end - ) - r_job_response_json = r_job_response.json() - instance.data["deadlineSubmissionJob"] = r_job_response_json - - # Store output dir for unified publisher (filesequence) - instance.data["outputDir"] = os.path.dirname( - render_path).replace("\\", "/") - instance.data["publishJobState"] = "Suspended" - - if instance.data.get("bakingNukeScripts"): - for baking_script in instance.data["bakingNukeScripts"]: - render_path = baking_script["bakeRenderPath"] - script_path = baking_script["bakeScriptPath"] - exe_node_name = baking_script["bakeWriteNodeName"] - - b_job_response = self.payload_submit( - instance, - script_path, - render_path, - exe_node_name, - submit_frame_start, - submit_frame_end, - r_job_response_json, - baking_submission=True - ) - - # Store output dir for unified publisher (filesequence) - instance.data["deadlineSubmissionJob"] = b_job_response.json() - - instance.data["publishJobState"] = "Suspended" - - # add to list of job Id - if not instance.data.get("bakingSubmissionJobs"): - instance.data["bakingSubmissionJobs"] = [] - - instance.data["bakingSubmissionJobs"].append( - b_job_response.json()["_id"]) - - # redefinition of families - if "render" in instance.data["productType"]: - instance.data["family"] = "write" - instance.data["productType"] = "write" - families.insert(0, "render2d") - elif "prerender" in instance.data["productType"]: - instance.data["family"] = "write" - instance.data["productType"] = "write" - families.insert(0, "prerender") - instance.data["families"] = families - - def _get_published_workfile_path(self, context): - """This method is temporary while the class is not inherited from - AbstractSubmitDeadline""" - anatomy = context.data["anatomy"] - # WARNING Hardcoded template name 'default' > may not be used - publish_template = anatomy.get_template_item( - "publish", "default", "path" - ) - for instance in context: - if ( - instance.data["productType"] != "workfile" - # Disabled instances won't be integrated - or instance.data("publish") is False - ): - continue - template_data = instance.data["anatomyData"] - # Expect workfile instance has only one representation - representation = instance.data["representations"][0] - # Get workfile extension - repre_file = representation["files"] - self.log.info(repre_file) - ext = os.path.splitext(repre_file)[1].lstrip(".") - - # Fill template data - template_data["representation"] = representation["name"] - template_data["ext"] = ext - template_data["comment"] = None - - template_filled = publish_template.format(template_data) - script_path = os.path.normpath(template_filled) - self.log.info( - "Using published scene for render {}".format( - script_path - ) - ) - return script_path - - return None - - def payload_submit( - self, - instance, - script_path, - render_path, - exe_node_name, - start_frame, - end_frame, - response_data=None, - baking_submission=False, - ): - """Submit payload to Deadline - - Args: - instance (pyblish.api.Instance): pyblish instance - script_path (str): path to nuke script - render_path (str): path to rendered images - exe_node_name (str): name of the node to render - start_frame (int): start frame - end_frame (int): end frame - response_data Optional[dict]: response data from - previous submission - baking_submission Optional[bool]: if it's baking submission - - Returns: - requests.Response - """ - render_dir = os.path.normpath(os.path.dirname(render_path)) - - # batch name - src_filepath = instance.context.data["currentFile"] - batch_name = os.path.basename(src_filepath) - job_name = os.path.basename(render_path) - - if is_in_tests(): - batch_name += datetime.now().strftime("%d%m%Y%H%M%S") - - output_filename_0 = self.preview_fname(render_path) - - if not response_data: - response_data = {} - - try: - # Ensure render folder exists - os.makedirs(render_dir) - except OSError: - pass - - # resolve any limit groups - limit_groups = self.get_limit_groups() - self.log.debug("Limit groups: `{}`".format(limit_groups)) - - payload = { - "JobInfo": { - # Top-level group name - "BatchName": batch_name, - - # Job name, as seen in Monitor - "Name": job_name, - - # Arbitrary username, for visualisation in Monitor - "UserName": self._deadline_user, - - "Priority": instance.data["attributeValues"].get( - "priority", self.priority), - "ChunkSize": instance.data["attributeValues"].get( - "chunk", self.chunk_size), - "ConcurrentTasks": instance.data["attributeValues"].get( - "concurrency", - self.concurrent_tasks - ), - - "Department": self.department, - - "Pool": instance.data.get("primaryPool"), - "SecondaryPool": instance.data.get("secondaryPool"), - "Group": self.group, - - "Plugin": "Nuke", - "Frames": "{start}-{end}".format( - start=start_frame, - end=end_frame - ), - "Comment": self._comment, - - # Optional, enable double-click to preview rendered - # frames from Deadline Monitor - "OutputFilename0": output_filename_0.replace("\\", "/"), - - # limiting groups - "LimitGroups": ",".join(limit_groups) - - }, - "PluginInfo": { - # Input - "SceneFile": script_path, - - # Output directory and filename - "OutputFilePath": render_dir.replace("\\", "/"), - # "OutputFilePrefix": render_variables["filename_prefix"], - - # Mandatory for Deadline - "Version": self._ver.group(), - - # Resolve relative references - "ProjectPath": script_path, - "AWSAssetFile0": render_path, - - # using GPU by default - "UseGpu": instance.data["attributeValues"].get( - "use_gpu", self.use_gpu), - - # Only the specific write node is rendered. - "WriteNode": exe_node_name - }, - - # Mandatory for Deadline, may be empty - "AuxFiles": [] - } - - # Add workfile dependency. - workfile_dependency = instance.data["attributeValues"].get( - "workfile_dependency", self.workfile_dependency - ) - if workfile_dependency: - payload["JobInfo"].update({"AssetDependency0": script_path}) - - # TODO: rewrite for baking with sequences - if baking_submission: - payload["JobInfo"].update({ - "JobType": "Normal", - "ChunkSize": 99999999 - }) - - if response_data.get("_id"): - payload["JobInfo"].update({ - "BatchName": response_data["Props"]["Batch"], - "JobDependency0": response_data["_id"], - }) - - # Include critical environment variables with submission - keys = [ - "PYTHONPATH", - "PATH", - "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_APP_NAME", - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "PYBLISHPLUGINPATH", - "NUKE_PATH", - "TOOL_ENV", - "FOUNDRY_LICENSE", - "OPENPYPE_SG_USER", - ] - - # add allowed keys from preset if any - if self.env_allowed_keys: - keys += self.env_allowed_keys - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - - # to recognize render jobs - environment["AYON_RENDER_JOB"] = "1" - - # finally search replace in values of any key - if self.env_search_replace_values: - for key, value in environment.items(): - for item in self.env_search_replace_values: - environment[key] = value.replace( - item["name"], item["value"] - ) - - payload["JobInfo"].update({ - "EnvironmentKeyValue%d" % index: "{key}={value}".format( - key=key, - value=environment[key] - ) for index, key in enumerate(environment) - }) - - plugin = payload["JobInfo"]["Plugin"] - self.log.debug("using render plugin : {}".format(plugin)) - - self.log.debug("Submitting..") - self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) - - # adding expected files to instance.data - self.expected_files( - instance, - render_path, - start_frame, - end_frame - ) - - self.log.debug("__ expectedFiles: `{}`".format( - instance.data["expectedFiles"])) - auth = instance.data["deadline"]["auth"] - verify = instance.data["deadline"]["verify"] - response = requests_post(self.deadline_url, - json=payload, - timeout=10, - auth=auth, - verify=verify) - - if not response.ok: - raise Exception(response.text) - - return response - - def preflight_check(self, instance): - """Ensure the startFrame, endFrame and byFrameStep are integers""" - - for key in ("frameStart", "frameEnd"): - value = instance.data[key] - - if int(value) == value: - continue - - self.log.warning( - "%f=%d was rounded off to nearest integer" - % (value, int(value)) - ) - - def preview_fname(self, path): - """Return output file path with #### for padding. - - Deadline requires the path to be formatted with # in place of numbers. - For example `/path/to/render.####.png` - - Args: - path (str): path to rendered images - - Returns: - str - - """ - self.log.debug("_ path: `{}`".format(path)) - if "%" in path: - search_results = re.search(r"(%0)(\d)(d.)", path).groups() - self.log.debug("_ search_results: `{}`".format(search_results)) - return int(search_results[1]) - if "#" in path: - self.log.debug("_ path: `{}`".format(path)) - return path - - def expected_files( - self, - instance, - filepath, - start_frame, - end_frame - ): - """ Create expected files in instance data - """ - if not instance.data.get("expectedFiles"): - instance.data["expectedFiles"] = [] - - dirname = os.path.dirname(filepath) - file = os.path.basename(filepath) - - # since some files might be already tagged as publish_on_farm - # we need to avoid adding them to expected files since those would be - # duplicated into metadata.json file - representations = instance.data.get("representations", []) - # check if file is not in representations with publish_on_farm tag - for repre in representations: - # Skip if 'publish_on_farm' not available - if "publish_on_farm" not in repre.get("tags", []): - continue - - # in case where single file (video, image) is already in - # representation file. Will be added to expected files via - # submit_publish_job.py - if file in repre.get("files", []): - self.log.debug( - "Skipping expected file: {}".format(filepath)) - return - - # in case path is hashed sequence expression - # (e.g. /path/to/file.####.png) - if "#" in file: - pparts = file.split("#") - padding = "%0{}d".format(len(pparts) - 1) - file = pparts[0] + padding + pparts[-1] - - # in case input path was single file (video or image) - if "%" not in file: - instance.data["expectedFiles"].append(filepath) - return - - # shift start frame by 1 if slate is present - if instance.data.get("slate"): - start_frame -= 1 - - # add sequence files to expected files - for i in range(start_frame, (end_frame + 1)): - instance.data["expectedFiles"].append( - os.path.join(dirname, (file % i)).replace("\\", "/")) - - def get_limit_groups(self): - """Search for limit group nodes and return group name. - Limit groups will be defined as pairs in Nuke deadline submitter - presents where the key will be name of limit group and value will be - a list of plugin's node class names. Thus, when a plugin uses more - than one node, these will be captured and the triggered process - will add the appropriate limit group to the payload jobinfo attributes. - Returning: - list: captured groups list - """ - # Not all hosts can import this module. - import nuke - - captured_groups = [] - for limit_group in self.limit_groups: - lg_name = limit_group["name"] - - for node_class in limit_group["value"]: - for node in nuke.allNodes(recurseGroups=True): - # ignore all nodes not member of defined class - if node.Class() not in node_class: - continue - # ignore all disabled nodes - if node["disable"].value(): - continue - # add group name if not already added - if lg_name not in captured_groups: - captured_groups.append(lg_name) - return captured_groups diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_publish_cache_job.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_publish_cache_job.py deleted file mode 100644 index d93592a6a3..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_publish_cache_job.py +++ /dev/null @@ -1,463 +0,0 @@ -# -*- coding: utf-8 -*- -"""Submit publishing job to farm.""" -import os -import json -import re -from copy import deepcopy - -import ayon_api -import pyblish.api - -from ayon_core.pipeline import publish -from ayon_core.lib import EnumDef, is_in_tests -from ayon_core.pipeline.version_start import get_versioning_start -from ayon_core.pipeline.farm.pyblish_functions import ( - create_skeleton_instance_cache, - create_instances_for_cache, - attach_instances_to_product, - prepare_cache_representations, - create_metadata_path -) -from ayon_deadline.abstract_submit_deadline import requests_post - - -class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, - publish.AYONPyblishPluginMixin, - publish.ColormanagedPyblishPluginMixin): - """Process Cache Job submitted on farm - This is replicated version of submit publish job - specifically for cache(s). - - These jobs are dependent on a deadline job - submission prior to this plug-in. - - - In case of Deadline, it creates dependent job on farm publishing - rendered image sequence. - - Options in instance.data: - - deadlineSubmissionJob (dict, Required): The returned .json - data from the job submission to deadline. - - - outputDir (str, Required): The output directory where the metadata - file should be generated. It's assumed that this will also be - final folder containing the output files. - - - ext (str, Optional): The extension (including `.`) that is required - in the output filename to be picked up for image sequence - publishing. - - - expectedFiles (list or dict): explained below - - """ - - label = "Submit cache jobs to Deadline" - order = pyblish.api.IntegratorOrder + 0.2 - icon = "tractor" - settings_category = "deadline" - - targets = ["local"] - - hosts = ["houdini"] - - families = ["publish.hou"] - - environ_keys = [ - "FTRACK_API_USER", - "FTRACK_API_KEY", - "FTRACK_SERVER", - "AYON_APP_NAME", - "AYON_USERNAME", - "AYON_SG_USERNAME", - "KITSU_LOGIN", - "KITSU_PWD" - ] - - # custom deadline attributes - deadline_department = "" - deadline_pool = "" - deadline_pool_secondary = "" - deadline_group = "" - deadline_chunk_size = 1 - deadline_priority = None - - # regex for finding frame number in string - R_FRAME_NUMBER = re.compile(r'.+\.(?P[0-9]+)\..+') - - plugin_pype_version = "3.0" - - # script path for publish_filesequence.py - publishing_script = None - - def _submit_deadline_post_job(self, instance, job): - """Submit publish job to Deadline. - - Returns: - (str): deadline_publish_job_id - """ - data = instance.data.copy() - product_name = data["productName"] - job_name = "Publish - {}".format(product_name) - - anatomy = instance.context.data['anatomy'] - - # instance.data.get("productName") != instances[0]["productName"] - # 'Main' vs 'renderMain' - override_version = None - instance_version = instance.data.get("version") # take this if exists - if instance_version != 1: - override_version = instance_version - - output_dir = self._get_publish_folder( - anatomy, - deepcopy(instance.data["anatomyData"]), - instance.data.get("folderEntity"), - instance.data["productName"], - instance.context, - instance.data["productType"], - override_version - ) - - # Transfer the environment from the original job to this dependent - # job so they use the same environment - metadata_path, rootless_metadata_path = \ - create_metadata_path(instance, anatomy) - - environment = { - "AYON_PROJECT_NAME": instance.context.data["projectName"], - "AYON_FOLDER_PATH": instance.context.data["folderPath"], - "AYON_TASK_NAME": instance.context.data["task"], - "AYON_USERNAME": instance.context.data["user"], - "AYON_LOG_NO_COLORS": "1", - "AYON_IN_TESTS": str(int(is_in_tests())), - "AYON_PUBLISH_JOB": "1", - "AYON_RENDER_JOB": "0", - "AYON_REMOTE_PUBLISH": "0", - "AYON_BUNDLE_NAME": os.environ["AYON_BUNDLE_NAME"], - "AYON_DEFAULT_SETTINGS_VARIANT": ( - os.environ["AYON_DEFAULT_SETTINGS_VARIANT"] - ), - } - - # add environments from self.environ_keys - for env_key in self.environ_keys: - if os.getenv(env_key): - environment[env_key] = os.environ[env_key] - - priority = self.deadline_priority or instance.data.get("priority", 50) - - instance_settings = self.get_attr_values_from_data(instance.data) - initial_status = instance_settings.get("publishJobState", "Active") - - args = [ - "--headless", - 'publish', - '"{}"'.format(rootless_metadata_path), - "--targets", "deadline", - "--targets", "farm" - ] - - # Generate the payload for Deadline submission - secondary_pool = ( - self.deadline_pool_secondary or instance.data.get("secondaryPool") - ) - payload = { - "JobInfo": { - "Plugin": "Ayon", - "BatchName": job["Props"]["Batch"], - "Name": job_name, - "UserName": job["Props"]["User"], - "Comment": instance.context.data.get("comment", ""), - - "Department": self.deadline_department, - "ChunkSize": self.deadline_chunk_size, - "Priority": priority, - "InitialStatus": initial_status, - - "Group": self.deadline_group, - "Pool": self.deadline_pool or instance.data.get("primaryPool"), - "SecondaryPool": secondary_pool, - # ensure the outputdirectory with correct slashes - "OutputDirectory0": output_dir.replace("\\", "/") - }, - "PluginInfo": { - "Version": self.plugin_pype_version, - "Arguments": " ".join(args), - "SingleFrameOnly": "True", - }, - # Mandatory for Deadline, may be empty - "AuxFiles": [], - } - - if job.get("_id"): - payload["JobInfo"]["JobDependency0"] = job["_id"] - - for index, (key_, value_) in enumerate(environment.items()): - payload["JobInfo"].update( - { - "EnvironmentKeyValue%d" - % index: "{key}={value}".format( - key=key_, value=value_ - ) - } - ) - # remove secondary pool - payload["JobInfo"].pop("SecondaryPool", None) - - self.log.debug("Submitting Deadline publish job ...") - - url = "{}/api/jobs".format(self.deadline_url) - auth = instance.data["deadline"]["auth"] - verify = instance.data["deadline"]["verify"] - response = requests_post( - url, json=payload, timeout=10, auth=auth, verify=verify) - if not response.ok: - raise Exception(response.text) - - deadline_publish_job_id = response.json()["_id"] - - return deadline_publish_job_id - - def process(self, instance): - # type: (pyblish.api.Instance) -> None - """Process plugin. - - Detect type of render farm submission and create and post dependent - job in case of Deadline. It creates json file with metadata needed for - publishing in directory of render. - - Args: - instance (pyblish.api.Instance): Instance data. - - """ - if not instance.data.get("farm"): - self.log.debug("Skipping local instance.") - return - - anatomy = instance.context.data["anatomy"] - - instance_skeleton_data = create_skeleton_instance_cache(instance) - """ - if content of `expectedFiles` list are dictionaries, we will handle - it as list of AOVs, creating instance for every one of them. - - Example: - -------- - - expectedFiles = [ - { - "beauty": [ - "foo_v01.0001.exr", - "foo_v01.0002.exr" - ], - - "Z": [ - "boo_v01.0001.exr", - "boo_v01.0002.exr" - ] - } - ] - - This will create instances for `beauty` and `Z` product - adding those files to their respective representations. - - If we have only list of files, we collect all file sequences. - More then one doesn't probably make sense, but we'll handle it - like creating one instance with multiple representations. - - Example: - -------- - - expectedFiles = [ - "foo_v01.0001.exr", - "foo_v01.0002.exr", - "xxx_v01.0001.exr", - "xxx_v01.0002.exr" - ] - - This will result in one instance with two representations: - `foo` and `xxx` - """ - - if isinstance(instance.data.get("expectedFiles")[0], dict): - instances = create_instances_for_cache( - instance, instance_skeleton_data) - else: - representations = prepare_cache_representations( - instance_skeleton_data, - instance.data.get("expectedFiles"), - anatomy - ) - - if "representations" not in instance_skeleton_data.keys(): - instance_skeleton_data["representations"] = [] - - # add representation - instance_skeleton_data["representations"] += representations - instances = [instance_skeleton_data] - - # attach instances to product - if instance.data.get("attachTo"): - instances = attach_instances_to_product( - instance.data.get("attachTo"), instances - ) - - r''' SUBMiT PUBLiSH JOB 2 D34DLiN3 - ____ - ' ' .---. .---. .--. .---. .--..--..--..--. .---. - | | --= \ | . \/ _|/ \| . \ || || \ |/ _| - | JOB | --= / | | || __| .. | | | |;_ || \ || __| - | | |____./ \.__|._||_.|___./|_____|||__|\__|\.___| - ._____. - - ''' - - render_job = None - submission_type = "" - if instance.data.get("toBeRenderedOn") == "deadline": - render_job = instance.data.pop("deadlineSubmissionJob", None) - submission_type = "deadline" - - if not render_job: - import getpass - - render_job = {} - self.log.debug("Faking job data ...") - render_job["Props"] = {} - # Render job doesn't exist because we do not have prior submission. - # We still use data from it so lets fake it. - # - # Batch name reflect original scene name - - if instance.data.get("assemblySubmissionJobs"): - render_job["Props"]["Batch"] = instance.data.get( - "jobBatchName") - else: - batch = os.path.splitext(os.path.basename( - instance.context.data.get("currentFile")))[0] - render_job["Props"]["Batch"] = batch - # User is deadline user - render_job["Props"]["User"] = instance.context.data.get( - "deadlineUser", getpass.getuser()) - - deadline_publish_job_id = None - if submission_type == "deadline": - self.deadline_url = instance.data["deadline"]["url"] - assert self.deadline_url, "Requires Deadline Webservice URL" - - deadline_publish_job_id = \ - self._submit_deadline_post_job(instance, render_job) - - # Inject deadline url to instances. - for inst in instances: - if "deadline" not in inst: - inst["deadline"] = {} - inst["deadline"] = instance.data["deadline"] - - # publish job file - publish_job = { - "folderPath": instance_skeleton_data["folderPath"], - "frameStart": instance_skeleton_data["frameStart"], - "frameEnd": instance_skeleton_data["frameEnd"], - "fps": instance_skeleton_data["fps"], - "source": instance_skeleton_data["source"], - "user": instance.context.data["user"], - "version": instance.context.data["version"], # workfile version - "intent": instance.context.data.get("intent"), - "comment": instance.context.data.get("comment"), - "job": render_job or None, - "instances": instances - } - - if deadline_publish_job_id: - publish_job["deadline_publish_job_id"] = deadline_publish_job_id - - metadata_path, rootless_metadata_path = \ - create_metadata_path(instance, anatomy) - - with open(metadata_path, "w") as f: - json.dump(publish_job, f, indent=4, sort_keys=True) - - def _get_publish_folder(self, anatomy, template_data, - folder_entity, product_name, context, - product_type, version=None): - """ - Extracted logic to pre-calculate real publish folder, which is - calculated in IntegrateNew inside of Deadline process. - This should match logic in: - 'collect_anatomy_instance_data' - to - get correct anatomy, family, version for product and - 'collect_resources_path' - get publish_path - - Args: - anatomy (ayon_core.pipeline.anatomy.Anatomy): - template_data (dict): pre-calculated collected data for process - folder_entity (dict[str, Any]): Folder entity. - product_name (str): Product name (actually group name of product). - product_type (str): for current deadline process it's always - 'render' - TODO - for generic use family needs to be dynamically - calculated like IntegrateNew does - version (int): override version from instance if exists - - Returns: - (string): publish folder where rendered and published files will - be stored - based on 'publish' template - """ - - project_name = context.data["projectName"] - host_name = context.data["hostName"] - if not version: - version_entity = None - if folder_entity: - version_entity = ayon_api.get_last_version_by_product_name( - project_name, - product_name, - folder_entity["id"] - ) - - if version_entity: - version = int(version_entity["version"]) + 1 - else: - version = get_versioning_start( - project_name, - host_name, - task_name=template_data["task"]["name"], - task_type=template_data["task"]["type"], - product_type="render", - product_name=product_name, - project_settings=context.data["project_settings"] - ) - - task_info = template_data.get("task") or {} - - template_name = publish.get_publish_template_name( - project_name, - host_name, - product_type, - task_info.get("name"), - task_info.get("type"), - ) - - template_data["subset"] = product_name - template_data["family"] = product_type - template_data["version"] = version - template_data["product"] = { - "name": product_name, - "type": product_type, - } - - render_dir_template = anatomy.get_template_item( - "publish", template_name, "directory" - ) - return render_dir_template.format_strict(template_data) - - @classmethod - def get_attribute_defs(cls): - return [ - EnumDef("publishJobState", - label="Publish Job State", - items=["Active", "Suspended"], - default="Active") - ] diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_publish_job.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_publish_job.py deleted file mode 100644 index 643dcc1c46..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_publish_job.py +++ /dev/null @@ -1,585 +0,0 @@ -# -*- coding: utf-8 -*- -"""Submit publishing job to farm.""" -import os -import json -import re -from copy import deepcopy - -import clique -import ayon_api -import pyblish.api - -from ayon_core.pipeline import publish -from ayon_core.lib import EnumDef, is_in_tests -from ayon_core.pipeline.version_start import get_versioning_start - -from ayon_core.pipeline.farm.pyblish_functions import ( - create_skeleton_instance, - create_instances_for_aov, - attach_instances_to_product, - prepare_representations, - create_metadata_path -) -from ayon_deadline.abstract_submit_deadline import requests_post - - -def get_resource_files(resources, frame_range=None): - """Get resource files at given path. - - If `frame_range` is specified those outside will be removed. - - Arguments: - resources (list): List of resources - frame_range (list): Frame range to apply override - - Returns: - list of str: list of collected resources - - """ - res_collections, _ = clique.assemble(resources) - assert len(res_collections) == 1, "Multiple collections found" - res_collection = res_collections[0] - - # Remove any frames - if frame_range is not None: - for frame in frame_range: - if frame not in res_collection.indexes: - continue - res_collection.indexes.remove(frame) - - return list(res_collection) - - -class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, - publish.AYONPyblishPluginMixin, - publish.ColormanagedPyblishPluginMixin): - """Process Job submitted on farm. - - These jobs are dependent on a deadline job - submission prior to this plug-in. - - It creates dependent job on farm publishing rendered image sequence. - - Options in instance.data: - - deadlineSubmissionJob (dict, Required): The returned .json - data from the job submission to deadline. - - - outputDir (str, Required): The output directory where the metadata - file should be generated. It's assumed that this will also be - final folder containing the output files. - - - ext (str, Optional): The extension (including `.`) that is required - in the output filename to be picked up for image sequence - publishing. - - - publishJobState (str, Optional): "Active" or "Suspended" - This defaults to "Suspended" - - - expectedFiles (list or dict): explained below - - """ - - label = "Submit Image Publishing job to Deadline" - order = pyblish.api.IntegratorOrder + 0.2 - icon = "tractor" - - targets = ["local"] - - hosts = ["fusion", "max", "maya", "nuke", "houdini", - "celaction", "aftereffects", "harmony", "blender"] - - families = ["render", "render.farm", "render.frames_farm", - "prerender", "prerender.farm", "prerender.frames_farm", - "renderlayer", "imagesequence", "image", - "vrayscene", "maxrender", - "arnold_rop", "mantra_rop", - "karma_rop", "vray_rop", - "redshift_rop", "usdrender"] - settings_category = "deadline" - - aov_filter = [ - { - "name": "maya", - "value": [r".*([Bb]eauty).*"] - }, - { - "name": "blender", - "value": [r".*([Bb]eauty).*"] - }, - { - # for everything from AE - "name": "aftereffects", - "value": [r".*"] - }, - { - "name": "harmony", - "value": [r".*"] - }, - { - "name": "celaction", - "value": [r".*"] - }, - { - "name": "max", - "value": [r".*"] - }, - ] - - environ_keys = [ - "FTRACK_API_USER", - "FTRACK_API_KEY", - "FTRACK_SERVER", - "AYON_APP_NAME", - "AYON_USERNAME", - "AYON_SG_USERNAME", - "KITSU_LOGIN", - "KITSU_PWD" - ] - - # custom deadline attributes - deadline_department = "" - deadline_pool = "" - deadline_pool_secondary = "" - deadline_group = "" - deadline_chunk_size = 1 - deadline_priority = None - - # regex for finding frame number in string - R_FRAME_NUMBER = re.compile(r'.+\.(?P[0-9]+)\..+') - - # mapping of instance properties to be transferred to new instance - # for every specified family - instance_transfer = { - "slate": ["slateFrames", "slate"], - "review": ["lutPath"], - "render2d": ["bakingNukeScripts", "version"], - "renderlayer": ["convertToScanline"] - } - - # list of family names to transfer to new family if present - families_transfer = ["render3d", "render2d", "ftrack", "slate"] - plugin_pype_version = "3.0" - - # script path for publish_filesequence.py - publishing_script = None - - # poor man exclusion - skip_integration_repre_list = [] - - def _submit_deadline_post_job(self, instance, job, instances): - """Submit publish job to Deadline. - - Returns: - (str): deadline_publish_job_id - """ - data = instance.data.copy() - product_name = data["productName"] - job_name = "Publish - {}".format(product_name) - - anatomy = instance.context.data['anatomy'] - - # instance.data.get("productName") != instances[0]["productName"] - # 'Main' vs 'renderMain' - override_version = None - instance_version = instance.data.get("version") # take this if exists - if instance_version != 1: - override_version = instance_version - - output_dir = self._get_publish_folder( - anatomy, - deepcopy(instance.data["anatomyData"]), - instance.data.get("folderEntity"), - instances[0]["productName"], - instance.context, - instances[0]["productType"], - override_version - ) - - # Transfer the environment from the original job to this dependent - # job so they use the same environment - metadata_path, rootless_metadata_path = \ - create_metadata_path(instance, anatomy) - - environment = { - "AYON_PROJECT_NAME": instance.context.data["projectName"], - "AYON_FOLDER_PATH": instance.context.data["folderPath"], - "AYON_TASK_NAME": instance.context.data["task"], - "AYON_USERNAME": instance.context.data["user"], - "AYON_LOG_NO_COLORS": "1", - "AYON_IN_TESTS": str(int(is_in_tests())), - "AYON_PUBLISH_JOB": "1", - "AYON_RENDER_JOB": "0", - "AYON_REMOTE_PUBLISH": "0", - "AYON_BUNDLE_NAME": os.environ["AYON_BUNDLE_NAME"], - "AYON_DEFAULT_SETTINGS_VARIANT": ( - os.environ["AYON_DEFAULT_SETTINGS_VARIANT"] - ), - } - - # add environments from self.environ_keys - for env_key in self.environ_keys: - if os.getenv(env_key): - environment[env_key] = os.environ[env_key] - - priority = self.deadline_priority or instance.data.get("priority", 50) - - instance_settings = self.get_attr_values_from_data(instance.data) - initial_status = instance_settings.get("publishJobState", "Active") - - args = [ - "--headless", - 'publish', - '"{}"'.format(rootless_metadata_path), - "--targets", "deadline", - "--targets", "farm" - ] - - # Generate the payload for Deadline submission - secondary_pool = ( - self.deadline_pool_secondary or instance.data.get("secondaryPool") - ) - payload = { - "JobInfo": { - "Plugin": "Ayon", - "BatchName": job["Props"]["Batch"], - "Name": job_name, - "UserName": job["Props"]["User"], - "Comment": instance.context.data.get("comment", ""), - - "Department": self.deadline_department, - "ChunkSize": self.deadline_chunk_size, - "Priority": priority, - "InitialStatus": initial_status, - - "Group": self.deadline_group, - "Pool": self.deadline_pool or instance.data.get("primaryPool"), - "SecondaryPool": secondary_pool, - # ensure the outputdirectory with correct slashes - "OutputDirectory0": output_dir.replace("\\", "/") - }, - "PluginInfo": { - "Version": self.plugin_pype_version, - "Arguments": " ".join(args), - "SingleFrameOnly": "True", - }, - # Mandatory for Deadline, may be empty - "AuxFiles": [], - } - - # add assembly jobs as dependencies - if instance.data.get("tileRendering"): - self.log.info("Adding tile assembly jobs as dependencies...") - job_index = 0 - for assembly_id in instance.data.get("assemblySubmissionJobs"): - payload["JobInfo"]["JobDependency{}".format( - job_index)] = assembly_id # noqa: E501 - job_index += 1 - elif instance.data.get("bakingSubmissionJobs"): - self.log.info( - "Adding baking submission jobs as dependencies..." - ) - job_index = 0 - for assembly_id in instance.data["bakingSubmissionJobs"]: - payload["JobInfo"]["JobDependency{}".format( - job_index)] = assembly_id # noqa: E501 - job_index += 1 - elif job.get("_id"): - payload["JobInfo"]["JobDependency0"] = job["_id"] - - for index, (key_, value_) in enumerate(environment.items()): - payload["JobInfo"].update( - { - "EnvironmentKeyValue%d" - % index: "{key}={value}".format( - key=key_, value=value_ - ) - } - ) - # remove secondary pool - payload["JobInfo"].pop("SecondaryPool", None) - - self.log.debug("Submitting Deadline publish job ...") - - url = "{}/api/jobs".format(self.deadline_url) - auth = instance.data["deadline"]["auth"] - verify = instance.data["deadline"]["verify"] - response = requests_post( - url, json=payload, timeout=10, auth=auth, verify=verify) - if not response.ok: - raise Exception(response.text) - - deadline_publish_job_id = response.json()["_id"] - - return deadline_publish_job_id - - def process(self, instance): - # type: (pyblish.api.Instance) -> None - """Process plugin. - - Detect type of render farm submission and create and post dependent - job in case of Deadline. It creates json file with metadata needed for - publishing in directory of render. - - Args: - instance (pyblish.api.Instance): Instance data. - - """ - if not instance.data.get("farm"): - self.log.debug("Skipping local instance.") - return - - anatomy = instance.context.data["anatomy"] - - instance_skeleton_data = create_skeleton_instance( - instance, families_transfer=self.families_transfer, - instance_transfer=self.instance_transfer) - """ - if content of `expectedFiles` list are dictionaries, we will handle - it as list of AOVs, creating instance for every one of them. - - Example: - -------- - - expectedFiles = [ - { - "beauty": [ - "foo_v01.0001.exr", - "foo_v01.0002.exr" - ], - - "Z": [ - "boo_v01.0001.exr", - "boo_v01.0002.exr" - ] - } - ] - - This will create instances for `beauty` and `Z` product - adding those files to their respective representations. - - If we have only list of files, we collect all file sequences. - More then one doesn't probably make sense, but we'll handle it - like creating one instance with multiple representations. - - Example: - -------- - - expectedFiles = [ - "foo_v01.0001.exr", - "foo_v01.0002.exr", - "xxx_v01.0001.exr", - "xxx_v01.0002.exr" - ] - - This will result in one instance with two representations: - `foo` and `xxx` - """ - do_not_add_review = False - if instance.data.get("review") is False: - self.log.debug("Instance has review explicitly disabled.") - do_not_add_review = True - - aov_filter = { - item["name"]: item["value"] - for item in self.aov_filter - } - if isinstance(instance.data.get("expectedFiles")[0], dict): - instances = create_instances_for_aov( - instance, instance_skeleton_data, - aov_filter, - self.skip_integration_repre_list, - do_not_add_review - ) - else: - representations = prepare_representations( - instance_skeleton_data, - instance.data.get("expectedFiles"), - anatomy, - aov_filter, - self.skip_integration_repre_list, - do_not_add_review, - instance.context, - self - ) - - if "representations" not in instance_skeleton_data.keys(): - instance_skeleton_data["representations"] = [] - - # add representation - instance_skeleton_data["representations"] += representations - instances = [instance_skeleton_data] - - # attach instances to product - if instance.data.get("attachTo"): - instances = attach_instances_to_product( - instance.data.get("attachTo"), instances - ) - - r''' SUBMiT PUBLiSH JOB 2 D34DLiN3 - ____ - ' ' .---. .---. .--. .---. .--..--..--..--. .---. - | | --= \ | . \/ _|/ \| . \ || || \ |/ _| - | JOB | --= / | | || __| .. | | | |;_ || \ || __| - | | |____./ \.__|._||_.|___./|_____|||__|\__|\.___| - ._____. - - ''' - - render_job = instance.data.pop("deadlineSubmissionJob", None) - if not render_job and instance.data.get("tileRendering") is False: - raise AssertionError(("Cannot continue without valid " - "Deadline submission.")) - if not render_job: - import getpass - - render_job = {} - self.log.debug("Faking job data ...") - render_job["Props"] = {} - # Render job doesn't exist because we do not have prior submission. - # We still use data from it so lets fake it. - # - # Batch name reflect original scene name - - if instance.data.get("assemblySubmissionJobs"): - render_job["Props"]["Batch"] = instance.data.get( - "jobBatchName") - else: - batch = os.path.splitext(os.path.basename( - instance.context.data.get("currentFile")))[0] - render_job["Props"]["Batch"] = batch - # User is deadline user - render_job["Props"]["User"] = instance.context.data.get( - "deadlineUser", getpass.getuser()) - - render_job["Props"]["Env"] = { - "FTRACK_API_USER": os.environ.get("FTRACK_API_USER"), - "FTRACK_API_KEY": os.environ.get("FTRACK_API_KEY"), - "FTRACK_SERVER": os.environ.get("FTRACK_SERVER"), - } - - # get default deadline webservice url from deadline module - self.deadline_url = instance.data["deadline"]["url"] - assert self.deadline_url, "Requires Deadline Webservice URL" - - deadline_publish_job_id = \ - self._submit_deadline_post_job(instance, render_job, instances) - - # Inject deadline url to instances to query DL for job id for overrides - for inst in instances: - inst["deadline"] = instance.data["deadline"] - - # publish job file - publish_job = { - "folderPath": instance_skeleton_data["folderPath"], - "frameStart": instance_skeleton_data["frameStart"], - "frameEnd": instance_skeleton_data["frameEnd"], - "fps": instance_skeleton_data["fps"], - "source": instance_skeleton_data["source"], - "user": instance.context.data["user"], - "version": instance.context.data["version"], # workfile version - "intent": instance.context.data.get("intent"), - "comment": instance.context.data.get("comment"), - "job": render_job or None, - "instances": instances - } - - if deadline_publish_job_id: - publish_job["deadline_publish_job_id"] = deadline_publish_job_id - - # add audio to metadata file if available - audio_file = instance.context.data.get("audioFile") - if audio_file and os.path.isfile(audio_file): - publish_job.update({"audio": audio_file}) - - metadata_path, rootless_metadata_path = \ - create_metadata_path(instance, anatomy) - - with open(metadata_path, "w") as f: - json.dump(publish_job, f, indent=4, sort_keys=True) - - def _get_publish_folder(self, anatomy, template_data, - folder_entity, product_name, context, - product_type, version=None): - """ - Extracted logic to pre-calculate real publish folder, which is - calculated in IntegrateNew inside of Deadline process. - This should match logic in: - 'collect_anatomy_instance_data' - to - get correct anatomy, family, version for product name and - 'collect_resources_path' - get publish_path - - Args: - anatomy (ayon_core.pipeline.anatomy.Anatomy): - template_data (dict): pre-calculated collected data for process - folder_entity (dict[str, Any]): Folder entity. - product_name (string): Product name (actually group name - of product) - product_type (string): for current deadline process it's always - 'render' - TODO - for generic use family needs to be dynamically - calculated like IntegrateNew does - version (int): override version from instance if exists - - Returns: - (string): publish folder where rendered and published files will - be stored - based on 'publish' template - """ - - project_name = context.data["projectName"] - host_name = context.data["hostName"] - if not version: - version_entity = None - if folder_entity: - version_entity = ayon_api.get_last_version_by_product_name( - project_name, - product_name, - folder_entity["id"] - ) - - if version_entity: - version = int(version_entity["version"]) + 1 - else: - version = get_versioning_start( - project_name, - host_name, - task_name=template_data["task"]["name"], - task_type=template_data["task"]["type"], - product_type="render", - product_name=product_name, - project_settings=context.data["project_settings"] - ) - - host_name = context.data["hostName"] - task_info = template_data.get("task") or {} - - template_name = publish.get_publish_template_name( - project_name, - host_name, - product_type, - task_info.get("name"), - task_info.get("type"), - ) - - template_data["version"] = version - template_data["subset"] = product_name - template_data["family"] = product_type - template_data["product"] = { - "name": product_name, - "type": product_type, - } - - render_dir_template = anatomy.get_template_item( - "publish", template_name, "directory" - ) - return render_dir_template.format_strict(template_data) - - @classmethod - def get_attribute_defs(cls): - return [ - EnumDef("publishJobState", - label="Publish Job State", - items=["Active", "Suspended"], - default="Active") - ] diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_deadline_connection.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_deadline_connection.py deleted file mode 100644 index fd89e3a2a7..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_deadline_connection.py +++ /dev/null @@ -1,52 +0,0 @@ -import pyblish.api - -from ayon_core.pipeline import PublishXmlValidationError - -from ayon_deadline.abstract_submit_deadline import requests_get - - -class ValidateDeadlineConnection(pyblish.api.InstancePlugin): - """Validate Deadline Web Service is running""" - - label = "Validate Deadline Web Service" - order = pyblish.api.ValidatorOrder - hosts = ["maya", "nuke", "aftereffects", "harmony", "fusion"] - families = ["renderlayer", "render", "render.farm"] - - # cache - responses = {} - - def process(self, instance): - if not instance.data.get("farm"): - self.log.debug("Should not be processed on farm, skipping.") - return - - deadline_url = instance.data["deadline"]["url"] - assert deadline_url, "Requires Deadline Webservice URL" - - kwargs = {} - if instance.data["deadline"]["require_authentication"]: - auth = instance.data["deadline"]["auth"] - kwargs["auth"] = auth - - if not auth[0]: - raise PublishXmlValidationError( - self, - "Deadline requires authentication. " - "At least username is required to be set in " - "Site Settings.") - - if deadline_url not in self.responses: - self.responses[deadline_url] = requests_get(deadline_url, **kwargs) - - response = self.responses[deadline_url] - if response.status_code == 401: - raise PublishXmlValidationError( - self, - "Deadline requires authentication. " - "Provided credentials are not working. " - "Please change them in Site Settings") - assert response.ok, "Response must be ok" - assert response.text.startswith("Deadline Web Service "), ( - "Web service did not respond with 'Deadline Web Service'" - ) diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_deadline_pools.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_deadline_pools.py deleted file mode 100644 index c7445465c4..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_deadline_pools.py +++ /dev/null @@ -1,84 +0,0 @@ -import pyblish.api - -from ayon_core.pipeline import ( - PublishXmlValidationError, - OptionalPyblishPluginMixin -) - - -class ValidateDeadlinePools(OptionalPyblishPluginMixin, - pyblish.api.InstancePlugin): - """Validate primaryPool and secondaryPool on instance. - - Values are on instance based on value insertion when Creating instance or - by Settings in CollectDeadlinePools. - """ - - label = "Validate Deadline Pools" - order = pyblish.api.ValidatorOrder - families = ["rendering", - "render.farm", - "render.frames_farm", - "renderFarm", - "renderlayer", - "maxrender", - "publish.hou"] - optional = True - - # cache - pools_per_url = {} - - def process(self, instance): - if not self.is_active(instance.data): - return - - if not instance.data.get("farm"): - self.log.debug("Skipping local instance.") - return - - deadline_url = instance.data["deadline"]["url"] - addons_manager = instance.context.data["ayonAddonsManager"] - deadline_addon = addons_manager["deadline"] - pools = self.get_pools( - deadline_addon, - deadline_url, - instance.data["deadline"].get("auth") - ) - - invalid_pools = {} - primary_pool = instance.data.get("primaryPool") - if primary_pool and primary_pool not in pools: - invalid_pools["primary"] = primary_pool - - secondary_pool = instance.data.get("secondaryPool") - if secondary_pool and secondary_pool not in pools: - invalid_pools["secondary"] = secondary_pool - - if invalid_pools: - message = "\n".join( - "{} pool '{}' not available on Deadline".format(key.title(), - pool) - for key, pool in invalid_pools.items() - ) - raise PublishXmlValidationError( - plugin=self, - message=message, - formatting_data={"pools_str": ", ".join(pools)} - ) - - def get_pools(self, deadline_addon, deadline_url, auth): - if deadline_url not in self.pools_per_url: - self.log.debug( - "Querying available pools for Deadline url: {}".format( - deadline_url) - ) - pools = deadline_addon.get_deadline_pools( - deadline_url, auth=auth, log=self.log - ) - # some DL return "none" as a pool name - if "none" not in pools: - pools.append("none") - self.log.info("Available pools: {}".format(pools)) - self.pools_per_url[deadline_url] = pools - - return self.pools_per_url[deadline_url] diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_expected_and_rendered_files.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_expected_and_rendered_files.py deleted file mode 100644 index 3fd13cfa10..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_expected_and_rendered_files.py +++ /dev/null @@ -1,256 +0,0 @@ -import os -import requests - -import pyblish.api - -from ayon_core.lib import collect_frames -from ayon_deadline.abstract_submit_deadline import requests_get - - -class ValidateExpectedFiles(pyblish.api.InstancePlugin): - """Compare rendered and expected files""" - - label = "Validate rendered files from Deadline" - order = pyblish.api.ValidatorOrder - families = ["render"] - targets = ["deadline"] - - # check if actual frame range on render job wasn't different - # case when artists wants to render only subset of frames - allow_user_override = True - - def process(self, instance): - """Process all the nodes in the instance""" - - # get dependency jobs ids for retrieving frame list - dependent_job_ids = self._get_dependent_job_ids(instance) - - if not dependent_job_ids: - self.log.warning("No dependent jobs found for instance: {}" - "".format(instance)) - return - - # get list of frames from dependent jobs - frame_list = self._get_dependent_jobs_frames( - instance, dependent_job_ids) - - for repre in instance.data["representations"]: - expected_files = self._get_expected_files(repre) - - staging_dir = repre["stagingDir"] - existing_files = self._get_existing_files(staging_dir) - - if self.allow_user_override: - # We always check for user override because the user might have - # also overridden the Job frame list to be longer than the - # originally submitted frame range - # todo: We should first check if Job frame range was overridden - # at all so we don't unnecessarily override anything - file_name_template, frame_placeholder = \ - self._get_file_name_template_and_placeholder( - expected_files) - - if not file_name_template: - raise RuntimeError("Unable to retrieve file_name template" - "from files: {}".format(expected_files)) - - job_expected_files = self._get_job_expected_files( - file_name_template, - frame_placeholder, - frame_list) - - job_files_diff = job_expected_files.difference(expected_files) - if job_files_diff: - self.log.debug( - "Detected difference in expected output files from " - "Deadline job. Assuming an updated frame list by the " - "user. Difference: {}".format(sorted(job_files_diff)) - ) - - # Update the representation expected files - self.log.info("Update range from actual job range " - "to frame list: {}".format(frame_list)) - # single item files must be string not list - repre["files"] = (sorted(job_expected_files) - if len(job_expected_files) > 1 else - list(job_expected_files)[0]) - - # Update the expected files - expected_files = job_expected_files - - # We don't use set.difference because we do allow other existing - # files to be in the folder that we might not want to use. - missing = expected_files - existing_files - if missing: - raise RuntimeError( - "Missing expected files: {}\n" - "Expected files: {}\n" - "Existing files: {}".format( - sorted(missing), - sorted(expected_files), - sorted(existing_files) - ) - ) - - def _get_dependent_job_ids(self, instance): - """Returns list of dependent job ids from instance metadata.json - - Args: - instance (pyblish.api.Instance): pyblish instance - - Returns: - (list): list of dependent job ids - - """ - dependent_job_ids = [] - - # job_id collected from metadata.json - original_job_id = instance.data["render_job_id"] - - dependent_job_ids_env = os.environ.get("RENDER_JOB_IDS") - if dependent_job_ids_env: - dependent_job_ids = dependent_job_ids_env.split(',') - elif original_job_id: - dependent_job_ids = [original_job_id] - - return dependent_job_ids - - def _get_dependent_jobs_frames(self, instance, dependent_job_ids): - """Returns list of frame ranges from all render job. - - Render job might be re-submitted so job_id in metadata.json could be - invalid. GlobalJobPreload injects current job id to RENDER_JOB_IDS. - - Args: - instance (pyblish.api.Instance): pyblish instance - dependent_job_ids (list): list of dependent job ids - Returns: - (list) - """ - all_frame_lists = [] - - for job_id in dependent_job_ids: - job_info = self._get_job_info(instance, job_id) - frame_list = job_info["Props"].get("Frames") - if frame_list: - all_frame_lists.extend(frame_list.split(',')) - - return all_frame_lists - - def _get_job_expected_files(self, - file_name_template, - frame_placeholder, - frame_list): - """Calculates list of names of expected rendered files. - - Might be different from expected files from submission if user - explicitly and manually changed the frame list on the Deadline job. - - """ - # no frames in file name at all, eg 'renderCompositingMain.withLut.mov' - if not frame_placeholder: - return {file_name_template} - - real_expected_rendered = set() - src_padding_exp = "%0{}d".format(len(frame_placeholder)) - for frames in frame_list: - if '-' not in frames: # single frame - frames = "{}-{}".format(frames, frames) - - start, end = frames.split('-') - for frame in range(int(start), int(end) + 1): - ren_name = file_name_template.replace( - frame_placeholder, src_padding_exp % frame) - real_expected_rendered.add(ren_name) - - return real_expected_rendered - - def _get_file_name_template_and_placeholder(self, files): - """Returns file name with frame replaced with # and this placeholder""" - sources_and_frames = collect_frames(files) - - file_name_template = frame_placeholder = None - for file_name, frame in sources_and_frames.items(): - - # There might be cases where clique was unable to collect - # collections in `collect_frames` - thus we capture that case - if frame is not None: - frame_placeholder = "#" * len(frame) - - file_name_template = os.path.basename( - file_name.replace(frame, frame_placeholder)) - else: - file_name_template = file_name - break - - return file_name_template, frame_placeholder - - def _get_job_info(self, instance, job_id): - """Calls DL for actual job info for 'job_id' - - Might be different than job info saved in metadata.json if user - manually changes job pre/during rendering. - - Args: - instance (pyblish.api.Instance): pyblish instance - job_id (str): Deadline job id - - Returns: - (dict): Job info from Deadline - - """ - deadline_url = instance.data["deadline"]["url"] - assert deadline_url, "Requires Deadline Webservice URL" - - url = "{}/api/jobs?JobID={}".format(deadline_url, job_id) - try: - kwargs = {} - auth = instance.data["deadline"]["auth"] - if auth: - kwargs["auth"] = auth - response = requests_get(url, **kwargs) - except requests.exceptions.ConnectionError: - self.log.error("Deadline is not accessible at " - "{}".format(deadline_url)) - return {} - - if not response.ok: - self.log.error("Submission failed!") - self.log.error(response.status_code) - self.log.error(response.content) - raise RuntimeError(response.text) - - json_content = response.json() - if json_content: - return json_content.pop() - return {} - - def _get_existing_files(self, staging_dir): - """Returns set of existing file names from 'staging_dir'""" - existing_files = set() - for file_name in os.listdir(staging_dir): - existing_files.add(file_name) - return existing_files - - def _get_expected_files(self, repre): - """Returns set of file names in representation['files'] - - The representations are collected from `CollectRenderedFiles` using - the metadata.json file submitted along with the render job. - - Args: - repre (dict): The representation containing 'files' - - Returns: - set: Set of expected file_names in the staging directory. - - """ - expected_files = set() - - files = repre["files"] - if not isinstance(files, list): - files = [files] - - for file_name in files: - expected_files.add(file_name) - return expected_files diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/Ayon/Ayon.ico b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/Ayon/Ayon.ico deleted file mode 100644 index aea977a1251232d0f3d78ea6cb124994f4d31eb4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7679 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 dg=}F#T++JjmkP-0+ukjp@", str(self.GetStartFrame()), - arguments) - arguments = re.sub(r"<(?i)ENDFRAME>", str(self.GetEndFrame()), - arguments) - arguments = re.sub(r"<(?i)QUOTE>", "\"", arguments) - - arguments = self.ReplacePaddedFrame(arguments, - "<(?i)STARTFRAME%([0-9]+)>", - self.GetStartFrame()) - arguments = self.ReplacePaddedFrame(arguments, - "<(?i)ENDFRAME%([0-9]+)>", - self.GetEndFrame()) - - count = 0 - for filename in self.GetAuxiliaryFilenames(): - localAuxFile = Path.Combine(self.GetJobsDataDirectory(), filename) - arguments = re.sub(r"<(?i)AUXFILE" + str(count) + r">", - localAuxFile.replace("\\", "/"), arguments) - count += 1 - - return arguments - - def ReplacePaddedFrame(self, arguments, pattern, frame): - frameRegex = Regex(pattern) - while True: - frameMatch = frameRegex.Match(arguments) - if not frameMatch.Success: - break - paddingSize = int(frameMatch.Groups[1].Value) - if paddingSize > 0: - padding = StringUtils.ToZeroPaddedString( - frame, paddingSize, False) - else: - padding = str(frame) - arguments = arguments.replace( - frameMatch.Groups[0].Value, padding) - - return arguments - - def HandleProgress(self): - progress = float(self.GetRegexMatch(1)) - self.SetProgress(progress) diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.ico b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.ico deleted file mode 100644 index 39d61592fe1addb07ed3ef93de362370485a23b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 103192 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 JVj#NS{{bY%R3rcZ diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.param b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.param deleted file mode 100644 index 24c59d2005..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.param +++ /dev/null @@ -1,38 +0,0 @@ -[About] -Type=label -Label=About -Category=About Plugin -CategoryOrder=-1 -Index=0 -Default=Celaction Plugin for Deadline -Description=Not configurable - -[ConcurrentTasks] -Type=label -Label=ConcurrentTasks -Category=About Plugin -CategoryOrder=-1 -Index=0 -Default=True -Description=Not configurable - -[Executable] -Type=filename -Label=Executable -Category=Config -CategoryOrder=0 -CategoryIndex=0 -Description=The command executable to run -Required=false -DisableIfBlank=true - -[RenderNameSeparator] -Type=string -Label=RenderNameSeparator -Category=Config -CategoryOrder=0 -CategoryIndex=1 -Description=The separator to use for naming -Required=false -DisableIfBlank=true -Default=. diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.py b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.py deleted file mode 100644 index 2d0edd3dca..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.py +++ /dev/null @@ -1,122 +0,0 @@ -from System.Text.RegularExpressions import * - -from Deadline.Plugins import * -from Deadline.Scripting import * - -import _winreg - -###################################################################### -# This is the function that Deadline calls to get an instance of the -# main DeadlinePlugin class. -###################################################################### - - -def GetDeadlinePlugin(): - return CelActionPlugin() - - -def CleanupDeadlinePlugin(deadlinePlugin): - deadlinePlugin.Cleanup() - -###################################################################### -# This is the main DeadlinePlugin class for the CelAction plugin. -###################################################################### - - -class CelActionPlugin(DeadlinePlugin): - - def __init__(self): - self.InitializeProcessCallback += self.InitializeProcess - self.RenderExecutableCallback += self.RenderExecutable - self.RenderArgumentCallback += self.RenderArgument - self.StartupDirectoryCallback += self.StartupDirectory - - def Cleanup(self): - for stdoutHandler in self.StdoutHandlers: - del stdoutHandler.HandleCallback - - del self.InitializeProcessCallback - del self.RenderExecutableCallback - del self.RenderArgumentCallback - del self.StartupDirectoryCallback - - def GetCelActionRegistryKey(self): - # Modify registry for frame separation - path = r'Software\CelAction\CelAction2D\User Settings' - _winreg.CreateKey(_winreg.HKEY_CURRENT_USER, path) - regKey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, path, 0, - _winreg.KEY_ALL_ACCESS) - return regKey - - def GetSeparatorValue(self, regKey): - useSeparator, _ = _winreg.QueryValueEx( - regKey, 'RenderNameUseSeparator') - separator, _ = _winreg.QueryValueEx(regKey, 'RenderNameSeparator') - - return useSeparator, separator - - def SetSeparatorValue(self, regKey, useSeparator, separator): - _winreg.SetValueEx(regKey, 'RenderNameUseSeparator', - 0, _winreg.REG_DWORD, useSeparator) - _winreg.SetValueEx(regKey, 'RenderNameSeparator', - 0, _winreg.REG_SZ, separator) - - def InitializeProcess(self): - # Set the plugin specific settings. - self.SingleFramesOnly = False - - # Set the process specific settings. - self.StdoutHandling = True - self.PopupHandling = True - - # Ignore 'celaction' Pop-up dialog - self.AddPopupIgnorer(".*Rendering.*") - self.AddPopupIgnorer(".*AutoRender.*") - - # Ignore 'celaction' Pop-up dialog - self.AddPopupIgnorer(".*Wait.*") - - # Ignore 'celaction' Pop-up dialog - self.AddPopupIgnorer(".*Timeline Scrub.*") - - celActionRegKey = self.GetCelActionRegistryKey() - - self.SetSeparatorValue(celActionRegKey, 1, self.GetConfigEntryWithDefault( - "RenderNameSeparator", ".").strip()) - - def RenderExecutable(self): - return RepositoryUtils.CheckPathMapping(self.GetConfigEntry("Executable").strip()) - - def RenderArgument(self): - arguments = RepositoryUtils.CheckPathMapping( - self.GetPluginInfoEntry("Arguments").strip()) - arguments = arguments.replace( - "", str(self.GetStartFrame())) - arguments = arguments.replace("", str(self.GetEndFrame())) - arguments = self.ReplacePaddedFrame( - arguments, "", self.GetStartFrame()) - arguments = self.ReplacePaddedFrame( - arguments, "", self.GetEndFrame()) - arguments = arguments.replace("", "\"") - return arguments - - def StartupDirectory(self): - return self.GetPluginInfoEntryWithDefault("StartupDirectory", "").strip() - - def ReplacePaddedFrame(self, arguments, pattern, frame): - frameRegex = Regex(pattern) - while True: - frameMatch = frameRegex.Match(arguments) - if frameMatch.Success: - paddingSize = int(frameMatch.Groups[1].Value) - if paddingSize > 0: - padding = StringUtils.ToZeroPaddedString( - frame, paddingSize, False) - else: - padding = str(frame) - arguments = arguments.replace( - frameMatch.Groups[0].Value, padding) - else: - break - - return arguments diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/GlobalJobPreLoad.py b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/GlobalJobPreLoad.py deleted file mode 100644 index dbd1798608..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ /dev/null @@ -1,662 +0,0 @@ -# /usr/bin/env python3 -# -*- coding: utf-8 -*- -import os -import tempfile -from datetime import datetime -import subprocess -import json -import platform -import uuid -import re -from Deadline.Scripting import ( - RepositoryUtils, - FileUtils, - DirectoryUtils, -) -__version__ = "1.1.1" -VERSION_REGEX = re.compile( - r"(?P0|[1-9]\d*)" - r"\.(?P0|[1-9]\d*)" - r"\.(?P0|[1-9]\d*)" - r"(?:-(?P[a-zA-Z\d\-.]*))?" - r"(?:\+(?P[a-zA-Z\d\-.]*))?" -) - - -class OpenPypeVersion: - """Fake semver version class for OpenPype version purposes. - - The version - """ - def __init__(self, major, minor, patch, prerelease, origin=None): - self.major = major - self.minor = minor - self.patch = patch - self.prerelease = prerelease - - is_valid = True - if major is None or minor is None or patch is None: - is_valid = False - self.is_valid = is_valid - - if origin is None: - base = "{}.{}.{}".format(str(major), str(minor), str(patch)) - if not prerelease: - origin = base - else: - origin = "{}-{}".format(base, str(prerelease)) - - self.origin = origin - - @classmethod - def from_string(cls, version): - """Create an object of version from string. - - Args: - version (str): Version as a string. - - Returns: - Union[OpenPypeVersion, None]: Version object if input is nonempty - string otherwise None. - """ - - if not version: - return None - valid_parts = VERSION_REGEX.findall(version) - if len(valid_parts) != 1: - # Return invalid version with filled 'origin' attribute - return cls(None, None, None, None, origin=str(version)) - - # Unpack found version - major, minor, patch, pre, post = valid_parts[0] - prerelease = pre - # Post release is not important anymore and should be considered as - # part of prerelease - # - comparison is implemented to find suitable build and builds should - # never contain prerelease part so "not proper" parsing is - # acceptable for this use case. - if post: - prerelease = "{}+{}".format(pre, post) - - return cls( - int(major), int(minor), int(patch), prerelease, origin=version - ) - - def has_compatible_release(self, other): - """Version has compatible release as other version. - - Both major and minor versions must be exactly the same. In that case - a build can be considered as release compatible with any version. - - Args: - other (OpenPypeVersion): Other version. - - Returns: - bool: Version is release compatible with other version. - """ - - if self.is_valid and other.is_valid: - return self.major == other.major and self.minor == other.minor - return False - - def __bool__(self): - return self.is_valid - - def __repr__(self): - return "<{} {}>".format(self.__class__.__name__, self.origin) - - def __eq__(self, other): - if not isinstance(other, self.__class__): - return self.origin == other - return self.origin == other.origin - - def __lt__(self, other): - if not isinstance(other, self.__class__): - return None - - if not self.is_valid: - return True - - if not other.is_valid: - return False - - if self.origin == other.origin: - return None - - same_major = self.major == other.major - if not same_major: - return self.major < other.major - - same_minor = self.minor == other.minor - if not same_minor: - return self.minor < other.minor - - same_patch = self.patch == other.patch - if not same_patch: - return self.patch < other.patch - - if not self.prerelease: - return False - - if not other.prerelease: - return True - - pres = [self.prerelease, other.prerelease] - pres.sort() - return pres[0] == self.prerelease - - -def get_openpype_version_from_path(path, build=True): - """Get OpenPype version from provided path. - path (str): Path to scan. - build (bool, optional): Get only builds, not sources - - Returns: - Union[OpenPypeVersion, None]: version of OpenPype if found. - """ - - # fix path for application bundle on macos - if platform.system().lower() == "darwin": - path = os.path.join(path, "MacOS") - - version_file = os.path.join(path, "openpype", "version.py") - if not os.path.isfile(version_file): - return None - - # skip if the version is not build - exe = os.path.join(path, "openpype_console.exe") - if platform.system().lower() in ["linux", "darwin"]: - exe = os.path.join(path, "openpype_console") - - # if only builds are requested - if build and not os.path.isfile(exe): # noqa: E501 - print(" ! path is not a build: {}".format(path)) - return None - - version = {} - with open(version_file, "r") as vf: - exec(vf.read(), version) - - version_str = version.get("__version__") - if version_str: - return OpenPypeVersion.from_string(version_str) - return None - - -def get_openpype_executable(): - """Return OpenPype Executable from Event Plug-in Settings""" - config = RepositoryUtils.GetPluginConfig("OpenPype") - exe_list = config.GetConfigEntryWithDefault("OpenPypeExecutable", "") - dir_list = config.GetConfigEntryWithDefault( - "OpenPypeInstallationDirs", "") - - # clean '\ ' for MacOS pasting - if platform.system().lower() == "darwin": - exe_list = exe_list.replace("\\ ", " ") - dir_list = dir_list.replace("\\ ", " ") - return exe_list, dir_list - - -def get_openpype_versions(dir_list): - print(">>> Getting OpenPype executable ...") - openpype_versions = [] - - # special case of multiple install dirs - for dir_list in dir_list.split(","): - install_dir = DirectoryUtils.SearchDirectoryList(dir_list) - if install_dir: - print("--- Looking for OpenPype at: {}".format(install_dir)) - sub_dirs = [ - f.path for f in os.scandir(install_dir) - if f.is_dir() - ] - for subdir in sub_dirs: - version = get_openpype_version_from_path(subdir) - if not version: - continue - print(" - found: {} - {}".format(version, subdir)) - openpype_versions.append((version, subdir)) - return openpype_versions - - -def get_requested_openpype_executable( - exe, dir_list, requested_version -): - requested_version_obj = OpenPypeVersion.from_string(requested_version) - if not requested_version_obj: - print(( - ">>> Requested version '{}' does not match version regex '{}'" - ).format(requested_version, VERSION_REGEX)) - return None - - print(( - ">>> Scanning for compatible requested version {}" - ).format(requested_version)) - openpype_versions = get_openpype_versions(dir_list) - if not openpype_versions: - return None - - # if looking for requested compatible version, - # add the implicitly specified to the list too. - if exe: - exe_dir = os.path.dirname(exe) - print("Looking for OpenPype at: {}".format(exe_dir)) - version = get_openpype_version_from_path(exe_dir) - if version: - print(" - found: {} - {}".format(version, exe_dir)) - openpype_versions.append((version, exe_dir)) - - matching_item = None - compatible_versions = [] - for version_item in openpype_versions: - version, version_dir = version_item - if requested_version_obj.has_compatible_release(version): - compatible_versions.append(version_item) - if version == requested_version_obj: - # Store version item if version match exactly - # - break if is found matching version - matching_item = version_item - break - - if not compatible_versions: - return None - - compatible_versions.sort(key=lambda item: item[0]) - if matching_item: - version, version_dir = matching_item - print(( - "*** Found exact match build version {} in {}" - ).format(version_dir, version)) - - else: - version, version_dir = compatible_versions[-1] - - print(( - "*** Latest compatible version found is {} in {}" - ).format(version_dir, version)) - - # create list of executables for different platform and let - # Deadline decide. - exe_list = [ - os.path.join(version_dir, "openpype_console.exe"), - os.path.join(version_dir, "openpype_console"), - os.path.join(version_dir, "MacOS", "openpype_console") - ] - return FileUtils.SearchFileList(";".join(exe_list)) - - -def inject_openpype_environment(deadlinePlugin): - """ Pull env vars from OpenPype and push them to rendering process. - - Used for correct paths, configuration from OpenPype etc. - """ - job = deadlinePlugin.GetJob() - - print(">>> Injecting OpenPype environments ...") - try: - exe_list, dir_list = get_openpype_executable() - exe = FileUtils.SearchFileList(exe_list) - - requested_version = job.GetJobEnvironmentKeyValue("OPENPYPE_VERSION") - if requested_version: - exe = get_requested_openpype_executable( - exe, dir_list, requested_version - ) - if exe is None: - raise RuntimeError(( - "Cannot find compatible version available for version {}" - " requested by the job. Please add it through plugin" - " configuration in Deadline or install it to configured" - " directory." - ).format(requested_version)) - - if not exe: - raise RuntimeError(( - "OpenPype executable was not found in the semicolon " - "separated list \"{}\"." - "The path to the render executable can be configured" - " from the Plugin Configuration in the Deadline Monitor." - ).format(";".join(exe_list))) - - print("--- OpenPype executable: {}".format(exe)) - - # tempfile.TemporaryFile cannot be used because of locking - temp_file_name = "{}_{}.json".format( - datetime.utcnow().strftime("%Y%m%d%H%M%S%f"), - str(uuid.uuid1()) - ) - export_url = os.path.join(tempfile.gettempdir(), temp_file_name) - print(">>> Temporary path: {}".format(export_url)) - - args = [ - "--headless", - "extractenvironments", - export_url - ] - - add_kwargs = { - "project": job.GetJobEnvironmentKeyValue("AVALON_PROJECT"), - "asset": job.GetJobEnvironmentKeyValue("AVALON_ASSET"), - "task": job.GetJobEnvironmentKeyValue("AVALON_TASK"), - "app": job.GetJobEnvironmentKeyValue("AVALON_APP_NAME"), - "envgroup": "farm" - } - - # use legacy IS_TEST env var to mark automatic tests for OP - if job.GetJobEnvironmentKeyValue("IS_TEST"): - args.append("--automatic-tests") - - if all(add_kwargs.values()): - for key, value in add_kwargs.items(): - args.extend(["--{}".format(key), value]) - else: - raise RuntimeError(( - "Missing required env vars: AVALON_PROJECT, AVALON_ASSET," - " AVALON_TASK, AVALON_APP_NAME" - )) - - openpype_mongo = job.GetJobEnvironmentKeyValue("OPENPYPE_MONGO") - if openpype_mongo: - # inject env var for OP extractenvironments - # SetEnvironmentVariable is important, not SetProcessEnv... - deadlinePlugin.SetEnvironmentVariable("OPENPYPE_MONGO", - openpype_mongo) - - if not os.environ.get("OPENPYPE_MONGO"): - print(">>> Missing OPENPYPE_MONGO env var, process won't work") - - os.environ["AVALON_TIMEOUT"] = "5000" - - args_str = subprocess.list2cmdline(args) - print(">>> Executing: {} {}".format(exe, args_str)) - process_exitcode = deadlinePlugin.RunProcess( - exe, args_str, os.path.dirname(exe), -1 - ) - - if process_exitcode != 0: - raise RuntimeError( - "Failed to run OpenPype process to extract environments." - ) - - print(">>> Loading file ...") - with open(export_url) as fp: - contents = json.load(fp) - - for key, value in contents.items(): - deadlinePlugin.SetProcessEnvironmentVariable(key, value) - - if "PATH" in contents: - # Set os.environ[PATH] so studio settings' path entries - # can be used to define search path for executables. - print(f">>> Setting 'PATH' Environment to: {contents['PATH']}") - os.environ["PATH"] = contents["PATH"] - - script_url = job.GetJobPluginInfoKeyValue("ScriptFilename") - if script_url: - script_url = script_url.format(**contents).replace("\\", "/") - print(">>> Setting script path {}".format(script_url)) - job.SetJobPluginInfoKeyValue("ScriptFilename", script_url) - - print(">>> Removing temporary file") - os.remove(export_url) - - print(">> Injection end.") - except Exception as e: - if hasattr(e, "output"): - print(">>> Exception {}".format(e.output)) - import traceback - print(traceback.format_exc()) - print("!!! Injection failed.") - RepositoryUtils.FailJob(job) - raise - - -def inject_ayon_environment(deadlinePlugin): - """ Pull env vars from AYON and push them to rendering process. - - Used for correct paths, configuration from AYON etc. - """ - job = deadlinePlugin.GetJob() - - print(">>> Injecting AYON environments ...") - try: - exe_list = get_ayon_executable() - exe = FileUtils.SearchFileList(exe_list) - - if not exe: - raise RuntimeError(( - "Ayon executable was not found in the semicolon " - "separated list \"{}\"." - "The path to the render executable can be configured" - " from the Plugin Configuration in the Deadline Monitor." - ).format(exe_list)) - - print("--- Ayon executable: {}".format(exe)) - - ayon_bundle_name = job.GetJobEnvironmentKeyValue("AYON_BUNDLE_NAME") - if not ayon_bundle_name: - raise RuntimeError( - "Missing env var in job properties AYON_BUNDLE_NAME" - ) - - config = RepositoryUtils.GetPluginConfig("Ayon") - ayon_server_url = ( - job.GetJobEnvironmentKeyValue("AYON_SERVER_URL") or - config.GetConfigEntryWithDefault("AyonServerUrl", "") - ) - ayon_api_key = ( - job.GetJobEnvironmentKeyValue("AYON_API_KEY") or - config.GetConfigEntryWithDefault("AyonApiKey", "") - ) - - if not all([ayon_server_url, ayon_api_key]): - raise RuntimeError(( - "Missing required values for server url and api key. " - "Please fill in Ayon Deadline plugin or provide by " - "AYON_SERVER_URL and AYON_API_KEY" - )) - - # tempfile.TemporaryFile cannot be used because of locking - temp_file_name = "{}_{}.json".format( - datetime.utcnow().strftime("%Y%m%d%H%M%S%f"), - str(uuid.uuid1()) - ) - export_url = os.path.join(tempfile.gettempdir(), temp_file_name) - print(">>> Temporary path: {}".format(export_url)) - - add_kwargs = { - "envgroup": "farm", - } - # Support backwards compatible keys - for key, env_keys in ( - ("project", ["AYON_PROJECT_NAME", "AVALON_PROJECT"]), - ("folder", ["AYON_FOLDER_PATH", "AVALON_ASSET"]), - ("task", ["AYON_TASK_NAME", "AVALON_TASK"]), - ("app", ["AYON_APP_NAME", "AVALON_APP_NAME"]), - ): - value = "" - for env_key in env_keys: - value = job.GetJobEnvironmentKeyValue(env_key) - if value: - break - add_kwargs[key] = value - - if not all(add_kwargs.values()): - raise RuntimeError(( - "Missing required env vars: AYON_PROJECT_NAME," - " AYON_FOLDER_PATH, AYON_TASK_NAME, AYON_APP_NAME" - )) - - # Use applications addon arguments - # TODO validate if applications addon should be used - args = [ - "--headless", - "addon", - "applications", - "extractenvironments", - export_url - ] - # Backwards compatibility for older versions - legacy_args = [ - "--headless", - "extractenvironments", - export_url - ] - - for key, value in add_kwargs.items(): - args.extend(["--{}".format(key), value]) - # Legacy arguments expect '--asset' instead of '--folder' - if key == "folder": - key = "asset" - legacy_args.extend(["--{}".format(key), value]) - - environment = { - "AYON_SERVER_URL": ayon_server_url, - "AYON_API_KEY": ayon_api_key, - "AYON_BUNDLE_NAME": ayon_bundle_name, - } - - automatic_tests = job.GetJobEnvironmentKeyValue("AYON_IN_TESTS") - if automatic_tests: - environment["AYON_IN_TESTS"] = automatic_tests - for env, val in environment.items(): - # Add the env var for the Render Plugin that is about to render - deadlinePlugin.SetEnvironmentVariable(env, val) - # Add the env var for current calls to `DeadlinePlugin.RunProcess` - deadlinePlugin.SetProcessEnvironmentVariable(env, val) - - args_str = subprocess.list2cmdline(args) - print(">>> Executing: {} {}".format(exe, args_str)) - process_exitcode = deadlinePlugin.RunProcess( - exe, args_str, os.path.dirname(exe), -1 - ) - - if process_exitcode != 0: - print( - "Failed to run AYON process to extract environments. Trying" - " to use legacy arguments." - ) - legacy_args_str = subprocess.list2cmdline(legacy_args) - process_exitcode = deadlinePlugin.RunProcess( - exe, legacy_args_str, os.path.dirname(exe), -1 - ) - if process_exitcode != 0: - raise RuntimeError( - "Failed to run AYON process to extract environments." - ) - - print(">>> Loading file ...") - with open(export_url) as fp: - contents = json.load(fp) - - for key, value in contents.items(): - deadlinePlugin.SetProcessEnvironmentVariable(key, value) - - if "PATH" in contents: - # Set os.environ[PATH] so studio settings' path entries - # can be used to define search path for executables. - print(f">>> Setting 'PATH' Environment to: {contents['PATH']}") - os.environ["PATH"] = contents["PATH"] - - script_url = job.GetJobPluginInfoKeyValue("ScriptFilename") - if script_url: - script_url = script_url.format(**contents).replace("\\", "/") - print(">>> Setting script path {}".format(script_url)) - job.SetJobPluginInfoKeyValue("ScriptFilename", script_url) - - print(">>> Removing temporary file") - os.remove(export_url) - - print(">> Injection end.") - except Exception as e: - if hasattr(e, "output"): - print(">>> Exception {}".format(e.output)) - import traceback - print(traceback.format_exc()) - print("!!! Injection failed.") - RepositoryUtils.FailJob(job) - raise - - -def get_ayon_executable(): - """Return AYON Executable from Event Plug-in Settings - - Returns: - list[str]: AYON executable paths. - - Raises: - RuntimeError: When no path configured at all. - - """ - config = RepositoryUtils.GetPluginConfig("Ayon") - exe_list = config.GetConfigEntryWithDefault("AyonExecutable", "") - - if not exe_list: - raise RuntimeError( - "Path to AYON executable not configured." - "Please set it in Ayon Deadline Plugin." - ) - - # clean '\ ' for MacOS pasting - if platform.system().lower() == "darwin": - exe_list = exe_list.replace("\\ ", " ") - - # Expand user paths - expanded_paths = [] - for path in exe_list.split(";"): - if path.startswith("~"): - path = os.path.expanduser(path) - expanded_paths.append(path) - return ";".join(expanded_paths) - - -def inject_render_job_id(deadlinePlugin): - """Inject dependency ids to publish process as env var for validation.""" - print(">>> Injecting render job id ...") - job = deadlinePlugin.GetJob() - - dependency_ids = job.JobDependencyIDs - print(">>> Dependency IDs: {}".format(dependency_ids)) - render_job_ids = ",".join(dependency_ids) - - deadlinePlugin.SetProcessEnvironmentVariable( - "RENDER_JOB_IDS", render_job_ids - ) - print(">>> Injection end.") - - -def __main__(deadlinePlugin): - print("*** GlobalJobPreload {} start ...".format(__version__)) - print(">>> Getting job ...") - job = deadlinePlugin.GetJob() - - openpype_render_job = job.GetJobEnvironmentKeyValue( - "OPENPYPE_RENDER_JOB") - openpype_publish_job = job.GetJobEnvironmentKeyValue( - "OPENPYPE_PUBLISH_JOB") - openpype_remote_job = job.GetJobEnvironmentKeyValue( - "OPENPYPE_REMOTE_PUBLISH") - - if openpype_publish_job == "1" and openpype_render_job == "1": - raise RuntimeError( - "Misconfiguration. Job couldn't be both render and publish." - ) - - if openpype_publish_job == "1": - inject_render_job_id(deadlinePlugin) - if openpype_render_job == "1" or openpype_remote_job == "1": - inject_openpype_environment(deadlinePlugin) - - ayon_render_job = job.GetJobEnvironmentKeyValue("AYON_RENDER_JOB") - ayon_publish_job = job.GetJobEnvironmentKeyValue("AYON_PUBLISH_JOB") - ayon_remote_job = job.GetJobEnvironmentKeyValue("AYON_REMOTE_PUBLISH") - - if ayon_publish_job == "1" and ayon_render_job == "1": - raise RuntimeError( - "Misconfiguration. Job couldn't be both render and publish." - ) - - if ayon_publish_job == "1": - inject_render_job_id(deadlinePlugin) - if ayon_render_job == "1" or ayon_remote_job == "1": - inject_ayon_environment(deadlinePlugin) diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.ico b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.ico deleted file mode 100644 index cf6f6bfcfa7cb19da3d76a576814a1473afc0d7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmbtUze@sP7=FaS;f8c5h?a(iT3Q++Y-k8U`U6@j=?A8Cb)lZ8|A7tFRzpK%KZc7k zm42wHK}56!&Q1-XjndR0*ZUzKd|jsy-tpY~zR&wSAMf{ZcYuv=@%+FN!N9I?M?6NgnA_@od)18#nCLsU~rTuPbmmz z6`Ykc7*;51XdW+PlFfb-!bv$!egO|LH1>TMFUIgC@;oQc5#RUjA_i%OKf3EJtY<@r za}STSW@B$21KiJIMuEorEEY7pFL)q%hvCRaQ5Ab>1*+&}xu|<2zb<2rYc6DXa}hk> zNAPqPMw)4PPmFpHv&>7nGCSAQCN@N5s18oSC3?*HZ5Y`DAKZl~KE#<>PBY}pdUNd= zgG6V$ZT8ZceIKr3)U3Cv$-iUvGtNqD#U=c%-hzr6UxW7YnV3PZm9`ysC1Z*EFJ-K> n)3xL2JG19iyyR58vb>$H6QA^B;(V?3j(5^^(=-1M{#*Y5EFN;S diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.options b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.options deleted file mode 100644 index efd44b4f94..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.options +++ /dev/null @@ -1,532 +0,0 @@ -[SceneFile] -Type=filename -Label=Scene Filename -Category=Global Settings -CategoryOrder=0 -Index=0 -Description=The scene filename as it exists on the network. -Required=false -DisableIfBlank=true - -[Environment] -Type=filename -Label=Scene Environment -Category=Global Settings -CategoryOrder=0 -Index=1 -Description=The Environment for the scene. -Required=false -DisableIfBlank=true - -[Job] -Type=filename -Label=Scene Job -Category=Global Settings -CategoryOrder=0 -Index=2 -Description=The Job that the scene belongs to. -Required=false -DisableIfBlank=true - -[SceneName] -Type=filename -Label=Scene Name -Category=Global Settings -CategoryOrder=0 -Index=3 -Description=The name of the scene to render -Required=false -DisableIfBlank=true - -[SceneVersion] -Type=filename -Label=Scene Version -Category=Global Settings -CategoryOrder=0 -Index=4 -Description=The version of the scene to render. -Required=false -DisableIfBlank=true - -[Version] -Type=enum -Values=10;11;12 -Label=Harmony Version -Category=Global Settings -CategoryOrder=0 -Index=5 -Description=The version of Harmony to use. -Required=false -DisableIfBlank=true - -[IsDatabase] -Type=Boolean -Label=Is Database Scene -Category=Global Settings -CategoryOrder=0 -Index=6 -Description=Whether or not the scene is in the database or not -Required=false -DisableIfBlank=true - -[Camera] -Type=string -Label=Camera -Category=Render Settings -CategoryOrder=1 -Index=0 -Description=Specifies the camera to use for rendering images. If Blank, the scene will be rendered with the current Camera. -Required=false -DisableIfBlank=true - -[UsingResPreset] -Type=Boolean -Label=Use Resolution Preset -Category=Render Settings -CategoryOrder=1 -Index=1 -Description=Whether or not you are using a resolution preset. -Required=false -DisableIfBlank=true - -[ResolutionName] -Type=enum -Values=HDTV_1080p24;HDTV_1080p25;HDTV_720p24;4K_UHD;8K_UHD;DCI_2K;DCI_4K;film-2K;film-4K;film-1.33_H;film-1.66_H;film-1.66_V;Cineon;NTSC;PAL;2160p;1440p;1080p;720p;480p;360p;240p;low;Web_Video;Game_512;Game_512_Ortho;WebCC_Preview;Custom -Label=Resolution Preset -Category=Render Settings -CategoryOrder=1 -Index=2 -Description=The resolution preset to use. -Required=true -Default=HDTV_1080p24 - -[PresetName] -Type=string -Label=Preset Name -Category=Render Settings -CategoryOrder=1 -Index=3 -Description=Specify the custom resolution name. -Required=true -Default= - -[ResolutionX] -Type=integer -Label=Resolution X -Minimum=0 -Maximum=1000000 -Category=Render Settings -CategoryOrder=1 -Index=4 -Description=Specifies the width of the rendered images. If 0, then the current resolution and Field of view will be used. -Required=true -Default=1920 - -[ResolutionY] -Type=integer -Label=Resolution Y -Minimum=0 -Maximum=1000000 -Category=Render Settings -CategoryOrder=1 -Index=5 -Description=Specifies the height of the rendered images. If 0, then the current resolution and Field of view will be used. -Required=true -Default=1080 - -[FieldOfView] -Type=float -Label=Field Of View -Minimum=0 -Maximum=89 -DecimalPlaces=2 -Category=Render Settings -CategoryOrder=1 -Index=6 -Description=Specifies the field of view of the rendered images. If 0, then the current resolution and Field of view will be used. -Required=true -Default=41.11 - -[Output0Node] -Type=string -Label=Render Node 0 Name -Category=Output Settings -CategoryOrder=2 -Index=0 -Description=The name of the render node. -Required=false -DisableIfBlank=true - -[Output0Type] -Type=enum -Values=Image;Movie -Label=Render Node 0 Type -Category=Output Settings -CategoryOrder=2 -Index=1 -Description=The type of output that the render node is producing. -Required=false -DisableIfBlank=true - -[Output0Path] -Type=string -Label=Render Node 0 Path -Category=Output Settings -CategoryOrder=2 -Index=2 -Description=The output path and file name of the output files. -Required=false -DisableIfBlank=true - -[Output0LeadingZero] -Type=integer -Label=Render Node 0 Leading Zeroes -Category=Output Settings -CategoryOrder=2 -Minimum=0 -Maximum=5 -Index=3 -Description=The number of leading zeroes for a 1 digit frame number. (1 less then the full padded length) -Required=false -DisableIfBlank=true - -[Output0Format] -Type=string -Label=Render Node 0 Format -Category=Output Settings -CategoryOrder=2 -Index=4 -Description=The format for the rendered output images. -Required=false -DisableIfBlank=true - -[Output0StartFrame] -Type=integer -Label=Render Node 0 Start Frame -Category=Output Settings -CategoryOrder=2 -Minimum=1 -Index=5 -Description=The frame that will correspond to frame one when numbering. If this value is not 1 then the monitor's job output features will not work properly. -Required=false -DisableIfBlank=true - -[Output1Node] -Type=string -Label=Render Node 1 Name -Category=Output Settings -CategoryOrder=2 -Index=6 -Description=The name of the render node. -Required=false -DisableIfBlank=true - -[Output1Type] -Type=enum -Values=Image;Movie -Label=Render Node 1 Type -Category=Output Settings -CategoryOrder=2 -Index=7 -Description=The type of output that the render node is producing. -Required=false -DisableIfBlank=true - -[Output1Path] -Type=string -Label=Render Node 1 Path -Category=Output Settings -CategoryOrder=2 -Index=8 -Description=The output path and file name of the output files. -Required=false -DisableIfBlank=true - -[Output1LeadingZero] -Type=integer -Label=Render Node 1 Leading Zeroes -Category=Output Settings -CategoryOrder=2 -Minimum=0 -Maximum=5 -Index=9 -Description=The number of leading zeroes for a 1 digit frame number. (1 less then the full padded length) -Required=false -DisableIfBlank=true - -[Output1Format] -Type=string -Label=Render Node 1 Format -Category=Output Settings -CategoryOrder=2 -Index=10 -Description=The format for the rendered output images. -Required=false -DisableIfBlank=true - -[Output1StartFrame] -Type=integer -Label=Render Node 1 Start Frame -Category=Output Settings -CategoryOrder=2 -Minimum=1 -Index=11 -Description=The frame that will correspond to frame one when numbering. If this value is not 1 then the monitor's job output features will not work properly. -Required=false -DisableIfBlank=true - -[Output2Node] -Type=string -Label=Render Node 2 Name -Category=Output Settings -CategoryOrder=2 -Index=12 -Description=The name of the render node. -Required=false -DisableIfBlank=true - -[Output2Type] -Type=enum -Values=Image;Movie -Label=Render Node 2 Type -Category=Output Settings -CategoryOrder=2 -Index=13 -Description=The type of output that the render node is producing. -Required=false -DisableIfBlank=true - -[Output2Path] -Type=string -Label=Render Node 2 Path -Category=Output Settings -CategoryOrder=2 -Index=14 -Description=The output path and file name of the output files. -Required=false -DisableIfBlank=true - -[Output2LeadingZero] -Type=integer -Label=Render Node 2 Leading Zeroes -Category=Output Settings -CategoryOrder=2 -Minimum=0 -Maximum=5 -Index=15 -Description=The number of leading zeroes for a 1 digit frame number. (1 less then the full padded length) -Required=false -DisableIfBlank=true - -[Output2Format] -Type=string -Label=Render Node 2 Format -Category=Output Settings -CategoryOrder=2 -Index=16 -Description=The format for the rendered output images. -Required=false -DisableIfBlank=true - -[Output2StartFrame] -Type=integer -Label=Render Node 2 Start Frame -Category=Output Settings -CategoryOrder=2 -Minimum=1 -Index=17 -Description=The frame that will correspond to frame one when numbering. If this value is not 1 then the monitor's job output features will not work properly. -Required=false -DisableIfBlank=true - -[Output3Node] -Type=string -Label=Render Node 3 Name -Category=Output Settings -CategoryOrder=2 -Index=18 -Description=The name of the render node. -Required=false -DisableIfBlank=true - -[Output3Type] -Type=enum -Values=Image;Movie -Label=Render Node 3 Type -Category=Output Settings -CategoryOrder=2 -Index=19 -Description=The type of output that the render node is producing. -Required=false -DisableIfBlank=true - -[Output3Path] -Type=string -Label=Render Node 3 Path -Category=Output Settings -CategoryOrder=2 -Index=20 -Description=The output path and file name of the output files. -Required=false -DisableIfBlank=true - -[Output3LeadingZero] -Type=integer -Label=Render Node 3 Leading Zeroes -Category=Output Settings -CategoryOrder=2 -Minimum=0 -Maximum=5 -Index=21 -Description=The number of leading zeroes for a 1 digit frame number. (1 less then the full padded length) -Required=false -DisableIfBlank=true - -[Output3Format] -Type=string -Label=Render Node 3 Format -Category=Output Settings -CategoryOrder=2 -Index=22 -Description=The format for the rendered output images. -Required=false -DisableIfBlank=true - -[Output3StartFrame] -Type=integer -Label=Render Node 3 Start Frame -Category=Output Settings -CategoryOrder=2 -Minimum=1 -Index=23 -Description=The frame that will correspond to frame one when numbering. If this value is not 1 then the monitor's job output features will not work properly. -Required=false -DisableIfBlank=true - -[Output4Node] -Type=string -Label=Render Node 4 Name -Category=Output Settings -CategoryOrder=2 -Index=24 -Description=The name of the render node. -Required=false -DisableIfBlank=true - -[Output4Type] -Type=enum -Values=Image;Movie -Label=Render Node 4 Type -Category=Output Settings -CategoryOrder=2 -Index=25 -Description=The type of output that the render node is producing. -Required=false -DisableIfBlank=true - -[Output4Path] -Type=string -Label=Render Node 4 Path -Category=Output Settings -CategoryOrder=2 -Index=26 -Description=The output path and file name of the output files. -Required=false -DisableIfBlank=true - -[Output4LeadingZero] -Type=integer -Label=Render Node 4 Leading Zeroes -Category=Output Settings -CategoryOrder=2 -Minimum=0 -Maximum=5 -Index=27 -Description=The number of leading zeroes for a 1 digit frame number. (1 less then the full padded length) -Required=false -DisableIfBlank=true - -[Output4Format] -Type=string -Label=Render Node 4 Format -Category=Output Settings -CategoryOrder=2 -Index=28 -Description=The format for the rendered output images. -Required=false -DisableIfBlank=true - -[Output4StartFrame] -Type=integer -Label=Render Node 4 Start Frame -Category=Output Settings -CategoryOrder=2 -Minimum=1 -Index=29 -Description=The frame that will correspond to frame one when numbering. If this value is not 1 then the monitor's job output features will not work properly. -Required=false -DisableIfBlank=true - -[Output5Node] -Type=string -Label=Render Node 5 Name -Category=Output Settings -CategoryOrder=2 -Index=30 -Description=The name of the render node. -Required=false -DisableIfBlank=true - -[Output5Type] -Type=enum -Values=Image;Movie -Label=Render Node 5 Type -Category=Output Settings -CategoryOrder=2 -Index=31 -Description=The type of output that the render node is producing. -Required=false -DisableIfBlank=true - -[Output5Path] -Type=string -Label=Render Node 5 Path -Category=Output Settings -CategoryOrder=2 -Index=32 -Description=The output path and file name of the output files. -Required=false -DisableIfBlank=true - -[Output5LeadingZero] -Type=integer -Label=Render Node 5 Leading Zeroes -Category=Output Settings -CategoryOrder=2 -Minimum=0 -Maximum=5 -Index=33 -Description=The number of leading zeroes for a 1 digit frame number. (1 less then the full padded length) -Required=false -DisableIfBlank=true - -[Output5Format] -Type=string -Label=Render Node 5 Format -Category=Output Settings -CategoryOrder=2 -Index=34 -Description=The format for the rendered output images. -Required=false -DisableIfBlank=true - -[Output5StartFrame] -Type=integer -Label=Render Node 5 Start Frame -Category=Output Settings -CategoryOrder=2 -Minimum=1 -Index=35 -Description=The frame that will correspond to frame one when numbering. If this value is not 1 then the monitor's job output features will not work properly. -Required=false -DisableIfBlank=true \ No newline at end of file diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.param b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.param deleted file mode 100644 index 43a54a464e..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.param +++ /dev/null @@ -1,98 +0,0 @@ -[About] -Type=label -Label=About -Category=About Plugin -CategoryOrder=-1 -Index=0 -Default=Harmony Render Plugin for Deadline -Description=Not configurable - -[ConcurrentTasks] -Type=label -Label=ConcurrentTasks -Category=About Plugin -CategoryOrder=-1 -Index=0 -Default=True -Description=Not configurable - -[Harmony_RenderExecutable_10] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=0 -Label=Harmony 10 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=C:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 10.0\win64\bin\Stage.exe - -[Harmony_RenderExecutable_11] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=1 -Label=Harmony 11 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=C:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 11.0\win64\bin\Stage.exe - -[Harmony_RenderExecutable_12] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=2 -Label=Harmony 12 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=C:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 12.0 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 12.0 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_12/lnx86_64/bin/HarmonyPremium - -[Harmony_RenderExecutable_14] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=3 -Label=Harmony 14 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=C:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 14.0 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 14.0 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_14/lnx86_64/bin/HarmonyPremium - -[Harmony_RenderExecutable_15] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=4 -Label=Harmony 15 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=C:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 15.0 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 15.0 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_15.0/lnx86_64/bin/HarmonyPremium - -[Harmony_RenderExecutable_17] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=4 -Label=Harmony 17 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=c:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 17 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 17 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_17/lnx86_64/bin/HarmonyPremium - -[Harmony_RenderExecutable_20] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=4 -Label=Harmony 20 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=c:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 20 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 20 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_20/lnx86_64/bin/HarmonyPremium - -[Harmony_RenderExecutable_21] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=4 -Label=Harmony 21 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=c:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 21 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 21 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_21/lnx86_64/bin/HarmonyPremium - -[Harmony_RenderExecutable_22] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=4 -Label=Harmony 22 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=c:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 22 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 22 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_22/lnx86_64/bin/HarmonyPremium diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.py b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.py deleted file mode 100644 index d9fd0b49ef..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env python3 -from System import * -from System.Diagnostics import * -from System.IO import * -from System.Text import * - -from Deadline.Plugins import * -from Deadline.Scripting import * - -def GetDeadlinePlugin(): - return HarmonyAYONPlugin() - -def CleanupDeadlinePlugin(deadlinePlugin): - deadlinePlugin.Cleanup() - -class HarmonyAYONPlugin(DeadlinePlugin): - - def __init__( self ): - super().__init__() - self.InitializeProcessCallback += self.InitializeProcess - self.RenderExecutableCallback += self.RenderExecutable - self.RenderArgumentCallback += self.RenderArgument - self.CheckExitCodeCallback += self.CheckExitCode - - def Cleanup( self ): - print("Cleanup") - for stdoutHandler in self.StdoutHandlers: - del stdoutHandler.HandleCallback - - del self.InitializeProcessCallback - del self.RenderExecutableCallback - del self.RenderArgumentCallback - - def CheckExitCode( self, exitCode ): - print("check code") - if exitCode != 0: - if exitCode == 100: - self.LogInfo( "Renderer reported an error with error code 100. This will be ignored, since the option to ignore it is specified in the Job Properties." ) - else: - self.FailRender( "Renderer returned non-zero error code %d. Check the renderer's output." % exitCode ) - - def InitializeProcess( self ): - self.PluginType = PluginType.Simple - self.StdoutHandling = True - self.PopupHandling = True - - self.AddStdoutHandlerCallback( "Rendered frame ([0-9]+)" ).HandleCallback += self.HandleStdoutProgress - - def HandleStdoutProgress( self ): - startFrame = self.GetStartFrame() - endFrame = self.GetEndFrame() - if( endFrame - startFrame + 1 != 0 ): - self.SetProgress( 100 * ( int(self.GetRegexMatch(1)) - startFrame + 1 ) / ( endFrame - startFrame + 1 ) ) - - def RenderExecutable( self ): - version = int( self.GetPluginInfoEntry( "Version" ) ) - exe = "" - exeList = self.GetConfigEntry( "Harmony_RenderExecutable_" + str(version) ) - exe = FileUtils.SearchFileList( exeList ) - if( exe == "" ): - self.FailRender( "Harmony render executable was not found in the configured separated list \"" + exeList + "\". The path to the render executable can be configured from the Plugin Configuration in the Deadline Monitor." ) - return exe - - def RenderArgument( self ): - renderArguments = "-batch" - - if self.GetBooleanPluginInfoEntryWithDefault( "UsingResPreset", False ): - resName = self.GetPluginInfoEntryWithDefault( "ResolutionName", "HDTV_1080p24" ) - if resName == "Custom": - renderArguments += " -res " + self.GetPluginInfoEntryWithDefault( "PresetName", "HDTV_1080p24" ) - else: - renderArguments += " -res " + resName - else: - resolutionX = self.GetIntegerPluginInfoEntryWithDefault( "ResolutionX", -1 ) - resolutionY = self.GetIntegerPluginInfoEntryWithDefault( "ResolutionY", -1 ) - fov = self.GetFloatPluginInfoEntryWithDefault( "FieldOfView", -1 ) - - if resolutionX > 0 and resolutionY > 0 and fov > 0: - renderArguments += " -res " + str( resolutionX ) + " " + str( resolutionY ) + " " + str( fov ) - - camera = self.GetPluginInfoEntryWithDefault( "Camera", "" ) - - if not camera == "": - renderArguments += " -camera " + camera - - startFrame = str( self.GetStartFrame() ) - endFrame = str( self.GetEndFrame() ) - - renderArguments += " -frames " + startFrame + " " + endFrame - - if not self.GetBooleanPluginInfoEntryWithDefault( "IsDatabase", False ): - sceneFilename = self.GetPluginInfoEntryWithDefault( "SceneFile", self.GetDataFilename() ) - sceneFilename = RepositoryUtils.CheckPathMapping( sceneFilename ) - renderArguments += " \"" + sceneFilename + "\"" - else: - environment = self.GetPluginInfoEntryWithDefault( "Environment", "" ) - renderArguments += " -env " + environment - job = self.GetPluginInfoEntryWithDefault( "Job", "" ) - renderArguments += " -job " + job - scene = self.GetPluginInfoEntryWithDefault( "SceneName", "" ) - renderArguments += " -scene " + scene - version = self.GetPluginInfoEntryWithDefault( "SceneVersion", "" ) - renderArguments += " -version " + version - - #tempSceneDirectory = self.CreateTempDirectory( "thread" + str(self.GetThreadNumber()) ) - #preRenderScript = - rendernodeNum = 0 - scriptBuilder = StringBuilder() - - while True: - nodeName = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "Node", "" ) - if nodeName == "": - break - nodeType = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "Type", "Image" ) - if nodeType == "Image": - nodePath = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "Path", "" ) - nodeLeadingZero = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "LeadingZero", "" ) - nodeFormat = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "Format", "" ) - nodeStartFrame = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "StartFrame", "" ) - - if not nodePath == "": - scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"drawingName\", 1, \"" + nodePath + "\" );") - - if not nodeLeadingZero == "": - scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"leadingZeros\", 1, \"" + nodeLeadingZero + "\" );") - - if not nodeFormat == "": - scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"drawingType\", 1, \"" + nodeFormat + "\" );") - - if not nodeStartFrame == "": - scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"start\", 1, \"" + nodeStartFrame + "\" );") - - if nodeType == "Movie": - nodePath = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "Path", "" ) - if not nodePath == "": - scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"moviePath\", 1, \"" + nodePath + "\" );") - - rendernodeNum += 1 - - tempDirectory = self.CreateTempDirectory( "thread" + str(self.GetThreadNumber()) ) - preRenderScriptName = Path.Combine( tempDirectory, "preRenderScript.txt" ) - - File.WriteAllText( preRenderScriptName, scriptBuilder.ToString() ) - - preRenderInlineScript = self.GetPluginInfoEntryWithDefault( "PreRenderInlineScript", "" ) - if preRenderInlineScript: - renderArguments += " -preRenderInlineScript \"" + preRenderInlineScript +"\"" - - renderArguments += " -preRenderScript \"" + preRenderScriptName +"\"" - - return renderArguments diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.ico b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.ico deleted file mode 100644 index de860673c4f25ed881e975ea4b17e8673cefb6c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126987 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 CpbIwu diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.options b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.options deleted file mode 100644 index 0b631ba66d..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.options +++ /dev/null @@ -1,35 +0,0 @@ -[OIIOToolPath] -Type=filename -Label=OIIO Tool location -Category=OIIO -Index=0 -Description=OIIO Tool executable to use. -Required=false -DisableIfBlank=true - -[OutputFile] -Type=filenamesave -Label=Output File -Category=Output -Index=0 -Description=The scene filename as it exists on the network -Required=false -DisableIfBlank=true - -[CleanupTiles] -Type=boolean -Category=Options -Index=0 -Label=Cleanup Tiles -Required=false -DisableIfBlank=true -Description=If enabled, the OpenPype Tile Assembler will cleanup all tiles after assembly. - -[Renderer] -Type=string -Label=Renderer -Category=Quicktime Info -Index=0 -Description=Renderer name -Required=false -DisableIfBlank=true diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.param b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.param deleted file mode 100644 index 66a3342e38..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.param +++ /dev/null @@ -1,17 +0,0 @@ -[About] -Type=label -Label=About -Category=About Plugin -CategoryOrder=-1 -Index=0 -Default=OpenPype Tile Assembler Plugin for Deadline -Description=Not configurable - -[OIIOTool_RenderExecutable] -Type=multilinemultifilename -Label=OIIO Tool Executable -Category=Render Executables -CategoryOrder=0 -Default=C:\Program Files\OIIO\bin\oiiotool.exe;/usr/bin/oiiotool -Description=The path to the Open Image IO Tool executable file used for rendering. Enter alternative paths on separate lines. -W diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py deleted file mode 100644 index f146aef7b4..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py +++ /dev/null @@ -1,457 +0,0 @@ -# -*- coding: utf-8 -*- -"""Tile Assembler Plugin using Open Image IO tool. - -Todo: - Currently we support only EXRs with their data window set. -""" -import os -import re -import subprocess -import xml.etree.ElementTree - -from System.IO import Path - -from Deadline.Plugins import DeadlinePlugin -from Deadline.Scripting import ( - FileUtils, RepositoryUtils, SystemUtils) - - -version_major = 1 -version_minor = 0 -version_patch = 0 -version_string = "{}.{}.{}".format(version_major, version_minor, version_patch) -STRING_TAGS = { - "format" -} -INT_TAGS = { - "x", "y", "z", - "width", "height", "depth", - "full_x", "full_y", "full_z", - "full_width", "full_height", "full_depth", - "tile_width", "tile_height", "tile_depth", - "nchannels", - "alpha_channel", - "z_channel", - "deep", - "subimages", -} - - -XML_CHAR_REF_REGEX_HEX = re.compile(r"&#x?[0-9a-fA-F]+;") - -# Regex to parse array attributes -ARRAY_TYPE_REGEX = re.compile(r"^(int|float|string)\[\d+\]$") - - -def convert_value_by_type_name(value_type, value): - """Convert value to proper type based on type name. - - In some cases value types have custom python class. - """ - - # Simple types - if value_type == "string": - return value - - if value_type == "int": - return int(value) - - if value_type == "float": - return float(value) - - # Vectors will probably have more types - if value_type in ("vec2f", "float2"): - return [float(item) for item in value.split(",")] - - # Matrix should be always have square size of element 3x3, 4x4 - # - are returned as list of lists - if value_type == "matrix": - output = [] - current_index = -1 - parts = value.split(",") - parts_len = len(parts) - if parts_len == 1: - divisor = 1 - elif parts_len == 4: - divisor = 2 - elif parts_len == 9: - divisor = 3 - elif parts_len == 16: - divisor = 4 - else: - print("Unknown matrix resolution {}. Value: \"{}\"".format( - parts_len, value - )) - for part in parts: - output.append(float(part)) - return output - - for idx, item in enumerate(parts): - list_index = idx % divisor - if list_index > current_index: - current_index = list_index - output.append([]) - output[list_index].append(float(item)) - return output - - if value_type == "rational2i": - parts = value.split("/") - top = float(parts[0]) - bottom = 1.0 - if len(parts) != 1: - bottom = float(parts[1]) - return float(top) / float(bottom) - - if value_type == "vector": - parts = [part.strip() for part in value.split(",")] - output = [] - for part in parts: - if part == "-nan": - output.append(None) - continue - try: - part = float(part) - except ValueError: - pass - output.append(part) - return output - - if value_type == "timecode": - return value - - # Array of other types is converted to list - re_result = ARRAY_TYPE_REGEX.findall(value_type) - if re_result: - array_type = re_result[0] - output = [] - for item in value.split(","): - output.append( - convert_value_by_type_name(array_type, item) - ) - return output - - print(( - "Dev note (missing implementation):" - " Unknown attrib type \"{}\". Value: {}" - ).format(value_type, value)) - return value - - -def parse_oiio_xml_output(xml_string): - """Parse xml output from OIIO info command.""" - output = {} - if not xml_string: - return output - - # Fix values with ampresand (lazy fix) - # - oiiotool exports invalid xml which ElementTree can't handle - # e.g. "" - # WARNING: this will affect even valid character entities. If you need - # those values correctly, this must take care of valid character ranges. - # See https://github.com/pypeclub/OpenPype/pull/2729 - matches = XML_CHAR_REF_REGEX_HEX.findall(xml_string) - for match in matches: - new_value = match.replace("&", "&") - xml_string = xml_string.replace(match, new_value) - - tree = xml.etree.ElementTree.fromstring(xml_string) - attribs = {} - output["attribs"] = attribs - for child in tree: - tag_name = child.tag - if tag_name == "attrib": - attrib_def = child.attrib - value = convert_value_by_type_name( - attrib_def["type"], child.text - ) - - attribs[attrib_def["name"]] = value - continue - - # Channels are stored as tex on each child - if tag_name == "channelnames": - value = [] - for channel in child: - value.append(channel.text) - - # Convert known integer type tags to int - elif tag_name in INT_TAGS: - value = int(child.text) - - # Keep value of known string tags - elif tag_name in STRING_TAGS: - value = child.text - - # Keep value as text for unknown tags - # - feel free to add more tags - else: - value = child.text - print(( - "Dev note (missing implementation):" - " Unknown tag \"{}\". Value \"{}\"" - ).format(tag_name, value)) - - output[child.tag] = value - - return output - - -def info_about_input(oiiotool_path, filepath): - args = [ - oiiotool_path, - "--info", - "-v", - "-i:infoformat=xml", - filepath - ] - popen = subprocess.Popen(args, stdout=subprocess.PIPE) - _stdout, _stderr = popen.communicate() - output = "" - if _stdout: - output += _stdout.decode("utf-8", errors="backslashreplace") - - if _stderr: - output += _stderr.decode("utf-8", errors="backslashreplace") - - output = output.replace("\r\n", "\n") - xml_started = False - lines = [] - for line in output.split("\n"): - if not xml_started: - if not line.startswith("<"): - continue - xml_started = True - if xml_started: - lines.append(line) - - if not xml_started: - raise ValueError( - "Failed to read input file \"{}\".\nOutput:\n{}".format( - filepath, output - ) - ) - xml_text = "\n".join(lines) - return parse_oiio_xml_output(xml_text) - - -def GetDeadlinePlugin(): # noqa: N802 - """Helper.""" - return OpenPypeTileAssembler() - - -def CleanupDeadlinePlugin(deadlinePlugin): # noqa: N802, N803 - """Helper.""" - deadlinePlugin.cleanup() - - -class OpenPypeTileAssembler(DeadlinePlugin): - """Deadline plugin for assembling tiles using OIIO.""" - - def __init__(self): - """Init.""" - super().__init__() - self.InitializeProcessCallback += self.initialize_process - self.RenderExecutableCallback += self.render_executable - self.RenderArgumentCallback += self.render_argument - self.PreRenderTasksCallback += self.pre_render_tasks - self.PostRenderTasksCallback += self.post_render_tasks - - def cleanup(self): - """Cleanup function.""" - for stdoutHandler in self.StdoutHandlers: - del stdoutHandler.HandleCallback - - del self.InitializeProcessCallback - del self.RenderExecutableCallback - del self.RenderArgumentCallback - del self.PreRenderTasksCallback - del self.PostRenderTasksCallback - - def initialize_process(self): - """Initialization.""" - self.LogInfo("Plugin version: {}".format(version_string)) - self.SingleFramesOnly = True - self.StdoutHandling = True - self.renderer = self.GetPluginInfoEntryWithDefault( - "Renderer", "undefined") - self.AddStdoutHandlerCallback( - ".*Error.*").HandleCallback += self.handle_stdout_error - - def render_executable(self): - """Get render executable name. - - Get paths from plugin configuration, find executable and return it. - - Returns: - (str): Render executable. - - """ - oiiotool_exe_list = self.GetConfigEntry("OIIOTool_RenderExecutable") - oiiotool_exe = FileUtils.SearchFileList(oiiotool_exe_list) - - if oiiotool_exe == "": - self.FailRender(("No file found in the semicolon separated " - "list \"{}\". The path to the render executable " - "can be configured from the Plugin Configuration " - "in the Deadline Monitor.").format( - oiiotool_exe_list)) - - return oiiotool_exe - - def render_argument(self): - """Generate command line arguments for render executable. - - Returns: - (str): arguments to add to render executable. - - """ - # Read tile config file. This file is in compatible format with - # Draft Tile Assembler - data = {} - with open(self.config_file, "rU") as f: - for text in f: - # Parsing key-value pair and removing white-space - # around the entries - info = [x.strip() for x in text.split("=", 1)] - - if len(info) > 1: - try: - data[str(info[0])] = info[1] - except Exception as e: - # should never be called - self.FailRender( - "Cannot parse config file: {}".format(e)) - - # Get output file. We support only EXRs now. - output_file = data["ImageFileName"] - output_file = RepositoryUtils.CheckPathMapping(output_file) - output_file = self.process_path(output_file) - - tile_info = [] - for tile in range(int(data["TileCount"])): - tile_info.append({ - "filepath": data["Tile{}".format(tile)], - "pos_x": int(data["Tile{}X".format(tile)]), - "pos_y": int(data["Tile{}Y".format(tile)]), - "height": int(data["Tile{}Height".format(tile)]), - "width": int(data["Tile{}Width".format(tile)]) - }) - - arguments = self.tile_oiio_args( - int(data["ImageWidth"]), int(data["ImageHeight"]), - tile_info, output_file) - self.LogInfo( - "Using arguments: {}".format(" ".join(arguments))) - self.tiles = tile_info - return " ".join(arguments) - - def process_path(self, filepath): - """Handle slashes in file paths.""" - if SystemUtils.IsRunningOnWindows(): - filepath = filepath.replace("/", "\\") - if filepath.startswith("\\") and not filepath.startswith("\\\\"): - filepath = "\\" + filepath - else: - filepath = filepath.replace("\\", "/") - return filepath - - def pre_render_tasks(self): - """Load config file and do remapping.""" - self.LogInfo("OpenPype Tile Assembler starting...") - config_file = self.GetPluginInfoEntry("ConfigFile") - - temp_scene_directory = self.CreateTempDirectory( - "thread" + str(self.GetThreadNumber())) - temp_scene_filename = Path.GetFileName(config_file) - self.config_file = Path.Combine( - temp_scene_directory, temp_scene_filename) - - if SystemUtils.IsRunningOnWindows(): - RepositoryUtils.CheckPathMappingInFileAndReplaceSeparator( - config_file, self.config_file, "/", "\\") - else: - RepositoryUtils.CheckPathMappingInFileAndReplaceSeparator( - config_file, self.config_file, "\\", "/") - os.chmod(self.config_file, os.stat(self.config_file).st_mode) - - def post_render_tasks(self): - """Cleanup tiles if required.""" - if self.GetBooleanPluginInfoEntryWithDefault("CleanupTiles", False): - self.LogInfo("Cleaning up Tiles...") - for tile in self.tiles: - try: - self.LogInfo("Deleting: {}".format(tile["filepath"])) - os.remove(tile["filepath"]) - # By this time we would have errored out - # if error on missing was enabled - except KeyError: - pass - except OSError: - self.LogInfo("Failed to delete: {}".format( - tile["filepath"])) - pass - - self.LogInfo("OpenPype Tile Assembler Job finished.") - - def handle_stdout_error(self): - """Handle errors in stdout.""" - self.FailRender(self.GetRegexMatch(0)) - - def tile_oiio_args( - self, output_width, output_height, tile_info, output_path): - """Generate oiio tool arguments for tile assembly. - - Args: - output_width (int): Width of output image. - output_height (int): Height of output image. - tile_info (list): List of tile items, each item must be - dictionary with `filepath`, `pos_x` and `pos_y` keys - representing path to file and x, y coordinates on output - image where top-left point of tile item should start. - output_path (str): Path to file where should be output stored. - - Returns: - (list): oiio tools arguments. - - """ - args = [] - - # Create new image with output resolution, and with same type and - # channels as input - oiiotool_path = self.render_executable() - first_tile_path = tile_info[0]["filepath"] - first_tile_info = info_about_input(oiiotool_path, first_tile_path) - create_arg_template = "--create{} {}x{} {}" - - image_type = "" - image_format = first_tile_info.get("format") - if image_format: - image_type = ":type={}".format(image_format) - - create_arg = create_arg_template.format( - image_type, output_width, - output_height, first_tile_info["nchannels"] - ) - args.append(create_arg) - - for tile in tile_info: - path = tile["filepath"] - pos_x = tile["pos_x"] - tile_height = info_about_input(oiiotool_path, path)["height"] - if self.renderer == "vray": - pos_y = tile["pos_y"] - else: - pos_y = output_height - tile["pos_y"] - tile_height - - # Add input path and make sure inputs origin is 0, 0 - args.append(path) - args.append("--origin +0+0") - # Swap to have input as foreground - args.append("--swap") - # Paste foreground to background - args.append("--paste {x:+d}{y:+d}".format(x=pos_x, y=pos_y)) - - args.append("-o") - args.append(output_path) - - return args diff --git a/server_addon/deadline/client/ayon_deadline/repository/readme.md b/server_addon/deadline/client/ayon_deadline/repository/readme.md deleted file mode 100644 index 31ffffd0b7..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/readme.md +++ /dev/null @@ -1,29 +0,0 @@ -## OpenPype Deadline repository overlay - - This directory is an overlay for Deadline repository. - It means that you can copy the whole hierarchy to Deadline repository and it - should work. - - Logic: - ----- - GlobalJobPreLoad - ----- - -The `GlobalJobPreLoad` will retrieve the OpenPype executable path from the -`OpenPype` Deadline Plug-in's settings. Then it will call the executable to -retrieve the environment variables needed for the Deadline Job. -These environment variables are injected into rendering process. - -Deadline triggers the `GlobalJobPreLoad.py` for each Worker as it starts the -Job. - -*Note*: It also contains backward compatible logic to preserve functionality -for old Pype2 and non-OpenPype triggered jobs. - - Plugin - ------ - For each render and publishing job the `OpenPype` Deadline Plug-in is checked - for the configured location of the OpenPype executable (needs to be configured - in `Deadline's Configure Plugins > OpenPype`) through `GlobalJobPreLoad`. - - diff --git a/server_addon/deadline/client/ayon_deadline/version.py b/server_addon/deadline/client/ayon_deadline/version.py deleted file mode 100644 index 96262d7186..0000000000 --- a/server_addon/deadline/client/ayon_deadline/version.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- -"""Package declaring AYON addon 'deadline' version.""" -__version__ = "0.2.3" diff --git a/server_addon/deadline/package.py b/server_addon/deadline/package.py deleted file mode 100644 index 8fcc007850..0000000000 --- a/server_addon/deadline/package.py +++ /dev/null @@ -1,10 +0,0 @@ -name = "deadline" -title = "Deadline" -version = "0.2.3" - -client_dir = "ayon_deadline" - -ayon_required_addons = { - "core": ">0.3.2", -} -ayon_compatible_addons = {} diff --git a/server_addon/deadline/server/__init__.py b/server_addon/deadline/server/__init__.py deleted file mode 100644 index 8d2dc152cd..0000000000 --- a/server_addon/deadline/server/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Type - -from ayon_server.addons import BaseServerAddon - -from .settings import DeadlineSettings, DEFAULT_VALUES, DeadlineSiteSettings - - -class Deadline(BaseServerAddon): - settings_model: Type[DeadlineSettings] = DeadlineSettings - site_settings_model: Type[DeadlineSiteSettings] = DeadlineSiteSettings - - - async def get_default_settings(self): - settings_model_cls = self.get_settings_model() - return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/deadline/server/settings/__init__.py b/server_addon/deadline/server/settings/__init__.py deleted file mode 100644 index d25c0fb330..0000000000 --- a/server_addon/deadline/server/settings/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .main import ( - DeadlineSettings, - DEFAULT_VALUES, -) -from .site_settings import DeadlineSiteSettings - - -__all__ = ( - "DeadlineSettings", - "DeadlineSiteSettings", - "DEFAULT_VALUES", -) diff --git a/server_addon/deadline/server/settings/main.py b/server_addon/deadline/server/settings/main.py deleted file mode 100644 index edb8a16e35..0000000000 --- a/server_addon/deadline/server/settings/main.py +++ /dev/null @@ -1,100 +0,0 @@ -from typing import TYPE_CHECKING -from pydantic import validator - -from ayon_server.settings import ( - BaseSettingsModel, - SettingsField, - ensure_unique_names, -) -if TYPE_CHECKING: - from ayon_server.addons import BaseServerAddon - -from .publish_plugins import ( - PublishPluginsModel, - DEFAULT_DEADLINE_PLUGINS_SETTINGS -) - - -async def defined_deadline_ws_name_enum_resolver( - addon: "BaseServerAddon", - settings_variant: str = "production", - project_name: str | None = None, -) -> list[str]: - """Provides list of names of configured Deadline webservice urls.""" - if addon is None: - return [] - - settings = await addon.get_studio_settings(variant=settings_variant) - - ws_server_name = [] - for deadline_url_item in settings.deadline_urls: - ws_server_name.append(deadline_url_item.name) - - return ws_server_name - -class ServerItemSubmodel(BaseSettingsModel): - """Connection info about configured DL servers.""" - _layout = "expanded" - name: str = SettingsField(title="Name") - value: str = SettingsField(title="Url") - require_authentication: bool = SettingsField( - False, title="Require authentication") - not_verify_ssl: bool = SettingsField( - False, title="Don't verify SSL") - default_username: str = SettingsField( - "", - title="Default user name", - description="Webservice username, 'Require authentication' must be " - "enabled." - ) - default_password: str = SettingsField( - "", - title="Default password", - description="Webservice password, 'Require authentication' must be " - "enabled." - ) - - -class DeadlineSettings(BaseSettingsModel): - # configured DL servers - deadline_urls: list[ServerItemSubmodel] = SettingsField( - default_factory=list, - title="System Deadline Webservice Info", - scope=["studio"], - ) - - # name(key) of selected server for project - deadline_server: str = SettingsField( - title="Project Deadline server name", - section="---", - scope=["project"], - enum_resolver=defined_deadline_ws_name_enum_resolver - ) - - publish: PublishPluginsModel = SettingsField( - default_factory=PublishPluginsModel, - title="Publish Plugins", - ) - - @validator("deadline_urls") - def validate_unique_names(cls, value): - ensure_unique_names(value) - return value - - - -DEFAULT_VALUES = { - "deadline_urls": [ - { - "name": "default", - "value": "http://127.0.0.1:8082", - "require_authentication": False, - "not_verify_ssl": False, - "default_username": "", - "default_password": "" - - } - ], - "deadline_server": "default", - "publish": DEFAULT_DEADLINE_PLUGINS_SETTINGS -} diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py deleted file mode 100644 index 1cf699db23..0000000000 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ /dev/null @@ -1,578 +0,0 @@ -from pydantic import validator - -from ayon_server.settings import ( - BaseSettingsModel, - SettingsField, - ensure_unique_names, -) - - -class CollectDeadlinePoolsModel(BaseSettingsModel): - """Settings Deadline default pools.""" - - primary_pool: str = SettingsField(title="Primary Pool") - - secondary_pool: str = SettingsField(title="Secondary Pool") - - -class ValidateExpectedFilesModel(BaseSettingsModel): - enabled: bool = SettingsField(True, title="Enabled") - active: bool = SettingsField(True, title="Active") - allow_user_override: bool = SettingsField( - True, title="Allow user change frame range" - ) - families: list[str] = SettingsField( - default_factory=list, title="Trigger on families" - ) - targets: list[str] = SettingsField( - default_factory=list, title="Trigger for plugins" - ) - - -def tile_assembler_enum(): - """Return a list of value/label dicts for the enumerator. - - Returning a list of dicts is used to allow for a custom label to be - displayed in the UI. - """ - return [ - { - "value": "DraftTileAssembler", - "label": "Draft Tile Assembler" - }, - { - "value": "OpenPypeTileAssembler", - "label": "Open Image IO" - } - ] - - -class ScenePatchesSubmodel(BaseSettingsModel): - _layout = "expanded" - name: str = SettingsField(title="Patch name") - regex: str = SettingsField(title="Patch regex") - line: str = SettingsField(title="Patch line") - - -class MayaSubmitDeadlineModel(BaseSettingsModel): - """Maya deadline submitter settings.""" - - enabled: bool = SettingsField(title="Enabled") - optional: bool = SettingsField(title="Optional") - active: bool = SettingsField(title="Active") - use_published: bool = SettingsField(title="Use Published scene") - import_reference: bool = SettingsField( - title="Use Scene with Imported Reference" - ) - asset_dependencies: bool = SettingsField(title="Use Asset dependencies") - priority: int = SettingsField(title="Priority") - tile_priority: int = SettingsField(title="Tile Priority") - group: str = SettingsField(title="Group") - limit: list[str] = SettingsField( - default_factory=list, - title="Limit Groups" - ) - tile_assembler_plugin: str = SettingsField( - title="Tile Assembler Plugin", - enum_resolver=tile_assembler_enum, - ) - jobInfo: str = SettingsField( - title="Additional JobInfo data", - widget="textarea", - ) - pluginInfo: str = SettingsField( - title="Additional PluginInfo data", - widget="textarea", - ) - - scene_patches: list[ScenePatchesSubmodel] = SettingsField( - default_factory=list, - title="Scene patches", - ) - strict_error_checking: bool = SettingsField( - title="Disable Strict Error Check profiles" - ) - - @validator("scene_patches") - def validate_unique_names(cls, value): - ensure_unique_names(value) - return value - - -class MaxSubmitDeadlineModel(BaseSettingsModel): - enabled: bool = SettingsField(True) - optional: bool = SettingsField(title="Optional") - active: bool = SettingsField(title="Active") - use_published: bool = SettingsField(title="Use Published scene") - priority: int = SettingsField(title="Priority") - chunk_size: int = SettingsField(title="Frame per Task") - group: str = SettingsField("", title="Group Name") - - -class EnvSearchReplaceSubmodel(BaseSettingsModel): - _layout = "compact" - name: str = SettingsField(title="Name") - value: str = SettingsField(title="Value") - - -class LimitGroupsSubmodel(BaseSettingsModel): - _layout = "expanded" - name: str = SettingsField(title="Name") - value: list[str] = SettingsField( - default_factory=list, - title="Limit Groups" - ) - - -def fusion_deadline_plugin_enum(): - """Return a list of value/label dicts for the enumerator. - - Returning a list of dicts is used to allow for a custom label to be - displayed in the UI. - """ - return [ - { - "value": "Fusion", - "label": "Fusion" - }, - { - "value": "FusionCmd", - "label": "FusionCmd" - } - ] - - -class FusionSubmitDeadlineModel(BaseSettingsModel): - enabled: bool = SettingsField(True, title="Enabled") - optional: bool = SettingsField(False, title="Optional") - active: bool = SettingsField(True, title="Active") - priority: int = SettingsField(50, title="Priority") - chunk_size: int = SettingsField(10, title="Frame per Task") - concurrent_tasks: int = SettingsField( - 1, title="Number of concurrent tasks" - ) - group: str = SettingsField("", title="Group Name") - plugin: str = SettingsField("Fusion", - enum_resolver=fusion_deadline_plugin_enum, - title="Deadline Plugin") - - -class NukeSubmitDeadlineModel(BaseSettingsModel): - """Nuke deadline submitter settings.""" - - enabled: bool = SettingsField(title="Enabled") - optional: bool = SettingsField(title="Optional") - active: bool = SettingsField(title="Active") - priority: int = SettingsField(title="Priority") - chunk_size: int = SettingsField(title="Chunk Size") - concurrent_tasks: int = SettingsField(title="Number of concurrent tasks") - group: str = SettingsField(title="Group") - department: str = SettingsField(title="Department") - use_gpu: bool = SettingsField(title="Use GPU") - workfile_dependency: bool = SettingsField(title="Workfile Dependency") - use_published_workfile: bool = SettingsField( - title="Use Published Workfile" - ) - - env_allowed_keys: list[str] = SettingsField( - default_factory=list, - title="Allowed environment keys" - ) - - env_search_replace_values: list[EnvSearchReplaceSubmodel] = SettingsField( - default_factory=list, - title="Search & replace in environment values", - ) - - limit_groups: list[LimitGroupsSubmodel] = SettingsField( - default_factory=list, - title="Limit Groups", - ) - - @validator( - "limit_groups", - "env_search_replace_values") - def validate_unique_names(cls, value): - ensure_unique_names(value) - return value - - -class HarmonySubmitDeadlineModel(BaseSettingsModel): - """Harmony deadline submitter settings.""" - - enabled: bool = SettingsField(title="Enabled") - optional: bool = SettingsField(title="Optional") - active: bool = SettingsField(title="Active") - use_published: bool = SettingsField(title="Use Published scene") - priority: int = SettingsField(title="Priority") - chunk_size: int = SettingsField(title="Chunk Size") - group: str = SettingsField(title="Group") - department: str = SettingsField(title="Department") - - -class HoudiniSubmitDeadlineModel(BaseSettingsModel): - """Houdini deadline render submitter settings.""" - enabled: bool = SettingsField(title="Enabled") - optional: bool = SettingsField(title="Optional") - active: bool = SettingsField(title="Active") - - priority: int = SettingsField(title="Priority") - chunk_size: int = SettingsField(title="Chunk Size") - group: str = SettingsField(title="Group") - - export_priority: int = SettingsField(title="Export Priority") - export_chunk_size: int = SettingsField(title="Export Chunk Size") - export_group: str = SettingsField(title="Export Group") - - -class HoudiniCacheSubmitDeadlineModel(BaseSettingsModel): - """Houdini deadline cache submitter settings.""" - enabled: bool = SettingsField(title="Enabled") - optional: bool = SettingsField(title="Optional") - active: bool = SettingsField(title="Active") - - priority: int = SettingsField(title="Priority") - chunk_size: int = SettingsField(title="Chunk Size") - group: str = SettingsField(title="Group") - - -class AfterEffectsSubmitDeadlineModel(BaseSettingsModel): - """After Effects deadline submitter settings.""" - - enabled: bool = SettingsField(title="Enabled") - optional: bool = SettingsField(title="Optional") - active: bool = SettingsField(title="Active") - use_published: bool = SettingsField(title="Use Published scene") - priority: int = SettingsField(title="Priority") - chunk_size: int = SettingsField(title="Chunk Size") - group: str = SettingsField(title="Group") - department: str = SettingsField(title="Department") - multiprocess: bool = SettingsField(title="Optional") - - -class CelactionSubmitDeadlineModel(BaseSettingsModel): - enabled: bool = SettingsField(True, title="Enabled") - deadline_department: str = SettingsField("", title="Deadline apartment") - deadline_priority: int = SettingsField(50, title="Deadline priority") - deadline_pool: str = SettingsField("", title="Deadline pool") - deadline_pool_secondary: str = SettingsField( - "", title="Deadline pool (secondary)" - ) - deadline_group: str = SettingsField("", title="Deadline Group") - deadline_chunk_size: int = SettingsField(10, title="Deadline Chunk size") - deadline_job_delay: str = SettingsField( - "", title="Delay job (timecode dd:hh:mm:ss)" - ) - - -class BlenderSubmitDeadlineModel(BaseSettingsModel): - enabled: bool = SettingsField(True) - optional: bool = SettingsField(title="Optional") - active: bool = SettingsField(title="Active") - use_published: bool = SettingsField(title="Use Published scene") - asset_dependencies: bool = SettingsField(title="Use Asset dependencies") - priority: int = SettingsField(title="Priority") - chunk_size: int = SettingsField(title="Frame per Task") - group: str = SettingsField("", title="Group Name") - job_delay: str = SettingsField( - "", title="Delay job (timecode dd:hh:mm:ss)" - ) - - -class AOVFilterSubmodel(BaseSettingsModel): - _layout = "expanded" - name: str = SettingsField(title="Host") - value: list[str] = SettingsField( - default_factory=list, - title="AOV regex" - ) - - -class ProcessCacheJobFarmModel(BaseSettingsModel): - """Process submitted job on farm.""" - - enabled: bool = SettingsField(title="Enabled") - deadline_department: str = SettingsField(title="Department") - deadline_pool: str = SettingsField(title="Pool") - deadline_group: str = SettingsField(title="Group") - deadline_chunk_size: int = SettingsField(title="Chunk Size") - deadline_priority: int = SettingsField(title="Priority") - - -class ProcessSubmittedJobOnFarmModel(BaseSettingsModel): - """Process submitted job on farm.""" - - enabled: bool = SettingsField(title="Enabled") - deadline_department: str = SettingsField(title="Department") - deadline_pool: str = SettingsField(title="Pool") - deadline_group: str = SettingsField(title="Group") - deadline_chunk_size: int = SettingsField(title="Chunk Size") - deadline_priority: int = SettingsField(title="Priority") - publishing_script: str = SettingsField(title="Publishing script path") - skip_integration_repre_list: list[str] = SettingsField( - default_factory=list, - title="Skip integration of representation with ext" - ) - families_transfer: list[str] = SettingsField( - default_factory=list, - title=( - "List of family names to transfer\n" - "to generated instances (AOVs for example)." - ) - ) - aov_filter: list[AOVFilterSubmodel] = SettingsField( - default_factory=list, - title="Reviewable products filter", - ) - - @validator("aov_filter") - def validate_unique_names(cls, value): - ensure_unique_names(value) - return value - - -class PublishPluginsModel(BaseSettingsModel): - CollectDeadlinePools: CollectDeadlinePoolsModel = SettingsField( - default_factory=CollectDeadlinePoolsModel, - title="Default Pools") - ValidateExpectedFiles: ValidateExpectedFilesModel = SettingsField( - default_factory=ValidateExpectedFilesModel, - title="Validate Expected Files" - ) - AfterEffectsSubmitDeadline: AfterEffectsSubmitDeadlineModel = ( - SettingsField( - default_factory=AfterEffectsSubmitDeadlineModel, - title="After Effects to deadline", - section="Hosts" - ) - ) - BlenderSubmitDeadline: BlenderSubmitDeadlineModel = SettingsField( - default_factory=BlenderSubmitDeadlineModel, - title="Blender Submit Deadline") - CelactionSubmitDeadline: CelactionSubmitDeadlineModel = SettingsField( - default_factory=CelactionSubmitDeadlineModel, - title="Celaction Submit Deadline") - FusionSubmitDeadline: FusionSubmitDeadlineModel = SettingsField( - default_factory=FusionSubmitDeadlineModel, - title="Fusion submit to Deadline") - HarmonySubmitDeadline: HarmonySubmitDeadlineModel = SettingsField( - default_factory=HarmonySubmitDeadlineModel, - title="Harmony Submit to deadline") - HoudiniCacheSubmitDeadline: HoudiniCacheSubmitDeadlineModel = SettingsField( - default_factory=HoudiniCacheSubmitDeadlineModel, - title="Houdini Submit cache to deadline") - HoudiniSubmitDeadline: HoudiniSubmitDeadlineModel = SettingsField( - default_factory=HoudiniSubmitDeadlineModel, - title="Houdini Submit render to deadline") - MaxSubmitDeadline: MaxSubmitDeadlineModel = SettingsField( - default_factory=MaxSubmitDeadlineModel, - title="Max Submit to deadline") - MayaSubmitDeadline: MayaSubmitDeadlineModel = SettingsField( - default_factory=MayaSubmitDeadlineModel, - title="Maya Submit to deadline") - NukeSubmitDeadline: NukeSubmitDeadlineModel = SettingsField( - default_factory=NukeSubmitDeadlineModel, - title="Nuke Submit to deadline") - ProcessSubmittedCacheJobOnFarm: ProcessCacheJobFarmModel = SettingsField( - default_factory=ProcessCacheJobFarmModel, - title="Process submitted cache Job on farm", - section="Publish Jobs") - ProcessSubmittedJobOnFarm: ProcessSubmittedJobOnFarmModel = SettingsField( - default_factory=ProcessSubmittedJobOnFarmModel, - title="Process submitted job on farm") - - -DEFAULT_DEADLINE_PLUGINS_SETTINGS = { - "CollectDeadlinePools": { - "primary_pool": "", - "secondary_pool": "" - }, - "ValidateExpectedFiles": { - "enabled": True, - "active": True, - "allow_user_override": True, - "families": [ - "render" - ], - "targets": [ - "deadline" - ] - }, - "AfterEffectsSubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "use_published": True, - "priority": 50, - "chunk_size": 10000, - "group": "", - "department": "", - "multiprocess": True - }, - "BlenderSubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "use_published": True, - "asset_dependencies": True, - "priority": 50, - "chunk_size": 10, - "group": "none", - "job_delay": "00:00:00:00" - }, - "CelactionSubmitDeadline": { - "enabled": True, - "deadline_department": "", - "deadline_priority": 50, - "deadline_pool": "", - "deadline_pool_secondary": "", - "deadline_group": "", - "deadline_chunk_size": 10, - "deadline_job_delay": "00:00:00:00" - }, - "FusionSubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "priority": 50, - "chunk_size": 10, - "concurrent_tasks": 1, - "group": "" - }, - "HarmonySubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "use_published": True, - "priority": 50, - "chunk_size": 10000, - "group": "", - "department": "" - }, - "HoudiniCacheSubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "priority": 50, - "chunk_size": 999999, - "group": "" - }, - "HoudiniSubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "priority": 50, - "chunk_size": 1, - "group": "", - "export_priority": 50, - "export_chunk_size": 10, - "export_group": "" - }, - "MaxSubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "use_published": True, - "priority": 50, - "chunk_size": 10, - "group": "none" - }, - "MayaSubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "tile_assembler_plugin": "DraftTileAssembler", - "use_published": True, - "import_reference": False, - "asset_dependencies": True, - "strict_error_checking": True, - "priority": 50, - "tile_priority": 50, - "group": "none", - "limit": [], - # this used to be empty dict - "jobInfo": "", - # this used to be empty dict - "pluginInfo": "", - "scene_patches": [] - }, - "NukeSubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "priority": 50, - "chunk_size": 10, - "concurrent_tasks": 1, - "group": "", - "department": "", - "use_gpu": True, - "workfile_dependency": True, - "use_published_workfile": True, - "env_allowed_keys": [], - "env_search_replace_values": [], - "limit_groups": [] - }, - "ProcessSubmittedCacheJobOnFarm": { - "enabled": True, - "deadline_department": "", - "deadline_pool": "", - "deadline_group": "", - "deadline_chunk_size": 1, - "deadline_priority": 50 - }, - "ProcessSubmittedJobOnFarm": { - "enabled": True, - "deadline_department": "", - "deadline_pool": "", - "deadline_group": "", - "deadline_chunk_size": 1, - "deadline_priority": 50, - "publishing_script": "", - "skip_integration_repre_list": [], - "families_transfer": ["render3d", "render2d", "ftrack", "slate"], - "aov_filter": [ - { - "name": "maya", - "value": [ - ".*([Bb]eauty).*" - ] - }, - { - "name": "blender", - "value": [ - ".*([Bb]eauty).*" - ] - }, - { - "name": "aftereffects", - "value": [ - ".*" - ] - }, - { - "name": "celaction", - "value": [ - ".*" - ] - }, - { - "name": "harmony", - "value": [ - ".*" - ] - }, - { - "name": "max", - "value": [ - ".*" - ] - }, - { - "name": "fusion", - "value": [ - ".*" - ] - } - ] - } -} diff --git a/server_addon/deadline/server/settings/site_settings.py b/server_addon/deadline/server/settings/site_settings.py deleted file mode 100644 index 92c092324e..0000000000 --- a/server_addon/deadline/server/settings/site_settings.py +++ /dev/null @@ -1,28 +0,0 @@ -from ayon_server.settings import ( - BaseSettingsModel, - SettingsField, -) - -from .main import defined_deadline_ws_name_enum_resolver - - -class CredentialPerServerModel(BaseSettingsModel): - """Provide credentials for configured DL servers""" - _layout = "expanded" - server_name: str = SettingsField( - "", - title="DL server name", - enum_resolver=defined_deadline_ws_name_enum_resolver - ) - username: str = SettingsField("", title="Username") - password: str = SettingsField("", title="Password") - - -class DeadlineSiteSettings(BaseSettingsModel): - local_settings: list[CredentialPerServerModel] = SettingsField( - default_factory=list, - title="Local setting", - description=( - "Please provide credentials for configured Deadline servers" - ), - ) From 285ad4cdb3d9282df909d71e55714341cd4146f6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 8 Jul 2024 19:59:33 +0200 Subject: [PATCH 12/14] Ignore invalid representation ids --- ...collect_input_representations_to_versions.py | 17 +++++++++++++++++ .../publish/collect_scene_loaded_versions.py | 17 ++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_input_representations_to_versions.py b/client/ayon_core/plugins/publish/collect_input_representations_to_versions.py index 770f3470c6..009acba89c 100644 --- a/client/ayon_core/plugins/publish/collect_input_representations_to_versions.py +++ b/client/ayon_core/plugins/publish/collect_input_representations_to_versions.py @@ -1,7 +1,18 @@ +import uuid + import ayon_api import pyblish.api +def is_valid_uuid(value) -> bool: + """Return whether value is a valid UUID""" + try: + uuid.UUID(value) + except ValueError: + return False + return True + + class CollectInputRepresentationsToVersions(pyblish.api.ContextPlugin): """Converts collected input representations to input versions. @@ -23,6 +34,12 @@ class CollectInputRepresentationsToVersions(pyblish.api.ContextPlugin): if inst_repre: representations.update(inst_repre) + # Ignore representation ids that are not valid + representations = { + representation_id for representation_id in representations + if is_valid_uuid(representation_id) + } + repre_entities = ayon_api.get_representations( project_name=context.data["projectName"], representation_ids=representations, diff --git a/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py b/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py index 1267c009e7..0a8fc93cf7 100644 --- a/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py +++ b/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py @@ -1,9 +1,20 @@ +import uuid + import ayon_api import pyblish.api from ayon_core.pipeline import registered_host +def is_valid_uuid(value) -> bool: + """Return whether value is a valid UUID""" + try: + uuid.UUID(value) + except ValueError: + return False + return True + + class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.0001 @@ -40,6 +51,10 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): container["representation"] for container in containers } + repre_ids = { + repre_id for repre_id in repre_ids + if is_valid_uuid(repre_id) + } project_name = context.data["projectName"] repre_entities = ayon_api.get_representations( @@ -65,7 +80,7 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): continue # NOTE: - # may have more then one representation that are same version + # may have more than one representation that are same version version = { "container_name": con["name"], "representation_id": repre_entity["id"], From a782ede959d3689e27d06e6cc991d5d31b1a2741 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 9 Jul 2024 10:09:57 +0200 Subject: [PATCH 13/14] Use `ayon_api.utils.convert_entity_id` to validate UUID --- ...llect_input_representations_to_versions.py | 14 ++------------ .../publish/collect_scene_loaded_versions.py | 19 +++++-------------- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_input_representations_to_versions.py b/client/ayon_core/plugins/publish/collect_input_representations_to_versions.py index 009acba89c..b9fe97b80b 100644 --- a/client/ayon_core/plugins/publish/collect_input_representations_to_versions.py +++ b/client/ayon_core/plugins/publish/collect_input_representations_to_versions.py @@ -1,18 +1,8 @@ -import uuid - import ayon_api +import ayon_api.utils import pyblish.api -def is_valid_uuid(value) -> bool: - """Return whether value is a valid UUID""" - try: - uuid.UUID(value) - except ValueError: - return False - return True - - class CollectInputRepresentationsToVersions(pyblish.api.ContextPlugin): """Converts collected input representations to input versions. @@ -37,7 +27,7 @@ class CollectInputRepresentationsToVersions(pyblish.api.ContextPlugin): # Ignore representation ids that are not valid representations = { representation_id for representation_id in representations - if is_valid_uuid(representation_id) + if ayon_api.utils.convert_entity_id(representation_id) } repre_entities = ayon_api.get_representations( diff --git a/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py b/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py index 0a8fc93cf7..7e955302c6 100644 --- a/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py +++ b/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py @@ -1,20 +1,9 @@ -import uuid - import ayon_api -import pyblish.api +import ayon_api.utils from ayon_core.pipeline import registered_host -def is_valid_uuid(value) -> bool: - """Return whether value is a valid UUID""" - try: - uuid.UUID(value) - except ValueError: - return False - return True - - class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.0001 @@ -51,9 +40,11 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): container["representation"] for container in containers } + + # Ignore representation ids that are not valid repre_ids = { - repre_id for repre_id in repre_ids - if is_valid_uuid(repre_id) + representation_id for representation_id in repre_ids + if ayon_api.utils.convert_entity_id(representation_id) } project_name = context.data["projectName"] From b25e3d0db63f21d0172decb5cec580cf3fdf6d76 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 9 Jul 2024 10:12:56 +0200 Subject: [PATCH 14/14] Fix broken import --- .../ayon_core/plugins/publish/collect_scene_loaded_versions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py b/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py index 7e955302c6..1abb8e29d2 100644 --- a/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py +++ b/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py @@ -2,6 +2,7 @@ import ayon_api import ayon_api.utils from ayon_core.pipeline import registered_host +import pyblish.api class CollectSceneLoadedVersions(pyblish.api.ContextPlugin):