From bbe21518a1ef46715064982f8c88b58b11d9f489 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 27 Apr 2020 16:49:55 +0200 Subject: [PATCH 001/131] feat(celaction): adding prelaunch --- pype/hooks/celaction/prelaunch.py | 91 +++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 pype/hooks/celaction/prelaunch.py diff --git a/pype/hooks/celaction/prelaunch.py b/pype/hooks/celaction/prelaunch.py new file mode 100644 index 0000000000..4ac0baca65 --- /dev/null +++ b/pype/hooks/celaction/prelaunch.py @@ -0,0 +1,91 @@ +import logging +import os +import _winreg +from pype.lib import PypeHook +from pypeapp import Logger + +log = logging.getLogger(__name__) + + +class CelactionPrelounchHook(PypeHook): + """ + This hook will check if current workfile path has Unreal + project inside. IF not, it initialize it and finally it pass + path to the project by environment variable to Unreal launcher + shell script. + """ + + def __init__(self, logger=None): + if not logger: + self.log = Logger().get_logger(self.__class__.__name__) + else: + self.log = logger + + self.signature = "( {} )".format(self.__class__.__name__) + + def execute(self, *args, env: dict = None) -> bool: + if not env: + env = os.environ + asset = env["AVALON_ASSET"] + task = env["AVALON_TASK"] + workdir = env["AVALON_WORKDIR"] + engine_version = env["AVALON_APP_NAME"].split("_")[-1] + project_name = f"{asset}_{task}" + + project_path = os.path.join(workdir, project_name) + + self.log.info((f"{self.signature} requested UE4 version: " + f"[ {engine_version} ]")) + + os.makedirs(project_path, exist_ok=True) + + project_file = os.path.join(project_path, f"{project_name}.uproject") + env["PYPE_UNREAL_PROJECT_FILE"] = project_file + + ########################## + # setting output parameters + path = r"Software\CelAction\CelAction2D\User Settings" + _winreg.CreateKey(_winreg.HKEY_CURRENT_USER, path) + hKey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, + r"Software\CelAction\CelAction2D\User Settings", + 0, _winreg.KEY_ALL_ACCESS) + + # TODO: change to root path and pyblish standalone to premiere way + root_path = os.getenv("PIPELINE_ROOT", os.path.dirname(__file__)) + path = os.path.join(root_path, "launchers", "pyblish_standalone.bat") + + _winreg.SetValueEx(hKey, "SubmitAppTitle", 0, _winreg.REG_SZ, path) + + parameters = " --path \"*SCENE*\" -d chunk *CHUNK* -d start *START*" + parameters += " -d end *END* -d x *X* -d y *Y* -rh celaction" + parameters += " -8 -d progpath \"*PROGPATH*\"" + _winreg.SetValueEx(hKey, "SubmitParametersTitle", 0, _winreg.REG_SZ, + parameters) + + # setting resolution parameters + path = r"Software\CelAction\CelAction2D\User Settings\Dialogs" + path += r"\SubmitOutput" + _winreg.CreateKey(_winreg.HKEY_CURRENT_USER, path) + hKey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, path, 0, + _winreg.KEY_ALL_ACCESS) + _winreg.SetValueEx(hKey, "SaveScene", 0, _winreg.REG_DWORD, 1) + _winreg.SetValueEx(hKey, "CustomX", 0, _winreg.REG_DWORD, 1920) + _winreg.SetValueEx(hKey, "CustomY", 0, _winreg.REG_DWORD, 1080) + + # making sure message dialogs don't appear when overwriting + path = r"Software\CelAction\CelAction2D\User Settings\Messages" + path += r"\OverwriteScene" + _winreg.CreateKey(_winreg.HKEY_CURRENT_USER, path) + hKey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, path, 0, + _winreg.KEY_ALL_ACCESS) + _winreg.SetValueEx(hKey, "Result", 0, _winreg.REG_DWORD, 6) + _winreg.SetValueEx(hKey, "Valid", 0, _winreg.REG_DWORD, 1) + + path = r"Software\CelAction\CelAction2D\User Settings\Messages" + path += r"\SceneSaved" + _winreg.CreateKey(_winreg.HKEY_CURRENT_USER, path) + hKey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, path, 0, + _winreg.KEY_ALL_ACCESS) + _winreg.SetValueEx(hKey, "Result", 0, _winreg.REG_DWORD, 1) + _winreg.SetValueEx(hKey, "Valid", 0, _winreg.REG_DWORD, 1) + return True From 1489e64077ec916b56fe62139dfd4c5d228f46b9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Apr 2020 11:38:54 +0200 Subject: [PATCH 002/131] feat(celaction): kick off integration --- pype/hooks/celaction/prelaunch.py | 15 ++++++--------- res/app_icons/celaction_local.png | Bin 0 -> 40783 bytes res/app_icons/celaction_remotel.png | Bin 0 -> 36400 bytes 3 files changed, 6 insertions(+), 9 deletions(-) create mode 100644 res/app_icons/celaction_local.png create mode 100644 res/app_icons/celaction_remotel.png diff --git a/pype/hooks/celaction/prelaunch.py b/pype/hooks/celaction/prelaunch.py index 4ac0baca65..a0adc26fb3 100644 --- a/pype/hooks/celaction/prelaunch.py +++ b/pype/hooks/celaction/prelaunch.py @@ -7,7 +7,7 @@ from pypeapp import Logger log = logging.getLogger(__name__) -class CelactionPrelounchHook(PypeHook): +class CelactionPrelaunchHook(PypeHook): """ This hook will check if current workfile path has Unreal project inside. IF not, it initialize it and finally it pass @@ -29,18 +29,14 @@ class CelactionPrelounchHook(PypeHook): asset = env["AVALON_ASSET"] task = env["AVALON_TASK"] workdir = env["AVALON_WORKDIR"] - engine_version = env["AVALON_APP_NAME"].split("_")[-1] project_name = f"{asset}_{task}" - project_path = os.path.join(workdir, project_name) + self.log.info(f"{self.signature}") - self.log.info((f"{self.signature} requested UE4 version: " - f"[ {engine_version} ]")) + os.makedirs(workdir, exist_ok=True) - os.makedirs(project_path, exist_ok=True) - - project_file = os.path.join(project_path, f"{project_name}.uproject") - env["PYPE_UNREAL_PROJECT_FILE"] = project_file + project_file = os.path.join(workdir, f"{project_name}.scn") + env["PYPE_CELACTION_PROJECT_FILE"] = project_file ########################## # setting output parameters @@ -88,4 +84,5 @@ class CelactionPrelounchHook(PypeHook): _winreg.KEY_ALL_ACCESS) _winreg.SetValueEx(hKey, "Result", 0, _winreg.REG_DWORD, 1) _winreg.SetValueEx(hKey, "Valid", 0, _winreg.REG_DWORD, 1) + return True diff --git a/res/app_icons/celaction_local.png b/res/app_icons/celaction_local.png new file mode 100644 index 0000000000000000000000000000000000000000..3a8abe6dbc07316d40c3a57834fb97925f2045ab GIT binary patch literal 40783 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 literal 0 HcmV?d00001 diff --git a/res/app_icons/celaction_remotel.png b/res/app_icons/celaction_remotel.png new file mode 100644 index 0000000000000000000000000000000000000000..320e8173eb90df65e0363f8784e4f034da0c76b8 GIT binary patch literal 36400 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 Date: Tue, 28 Apr 2020 18:11:53 +0200 Subject: [PATCH 003/131] fix(celaction): _winreg to winreg and additional prints --- pype/hooks/celaction/prelaunch.py | 55 +++++++++++++++++-------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/pype/hooks/celaction/prelaunch.py b/pype/hooks/celaction/prelaunch.py index a0adc26fb3..af48be350c 100644 --- a/pype/hooks/celaction/prelaunch.py +++ b/pype/hooks/celaction/prelaunch.py @@ -1,6 +1,6 @@ import logging import os -import _winreg +import winreg from pype.lib import PypeHook from pypeapp import Logger @@ -30,59 +30,64 @@ class CelactionPrelaunchHook(PypeHook): task = env["AVALON_TASK"] workdir = env["AVALON_WORKDIR"] project_name = f"{asset}_{task}" + version = "v001" self.log.info(f"{self.signature}") os.makedirs(workdir, exist_ok=True) + self.log.info(f"Work dir is: `{workdir}`") - project_file = os.path.join(workdir, f"{project_name}.scn") + project_file = os.path.join(workdir, f"{project_name}_{version}.scn") env["PYPE_CELACTION_PROJECT_FILE"] = project_file + self.log.info(f"Workfile is: `{project_file}`") + ########################## # setting output parameters path = r"Software\CelAction\CelAction2D\User Settings" - _winreg.CreateKey(_winreg.HKEY_CURRENT_USER, path) - hKey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, - r"Software\CelAction\CelAction2D\User Settings", - 0, _winreg.KEY_ALL_ACCESS) + winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) + hKey = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + "Software\\CelAction\\CelAction2D\\User Settings", 0, + winreg.KEY_ALL_ACCESS) # TODO: change to root path and pyblish standalone to premiere way root_path = os.getenv("PIPELINE_ROOT", os.path.dirname(__file__)) path = os.path.join(root_path, "launchers", "pyblish_standalone.bat") - _winreg.SetValueEx(hKey, "SubmitAppTitle", 0, _winreg.REG_SZ, path) + winreg.SetValueEx(hKey, "SubmitAppTitle", 0, winreg.REG_SZ, path) parameters = " --path \"*SCENE*\" -d chunk *CHUNK* -d start *START*" parameters += " -d end *END* -d x *X* -d y *Y* -rh celaction" parameters += " -8 -d progpath \"*PROGPATH*\"" - _winreg.SetValueEx(hKey, "SubmitParametersTitle", 0, _winreg.REG_SZ, - parameters) + winreg.SetValueEx(hKey, "SubmitParametersTitle", 0, winreg.REG_SZ, + parameters) # setting resolution parameters path = r"Software\CelAction\CelAction2D\User Settings\Dialogs" path += r"\SubmitOutput" - _winreg.CreateKey(_winreg.HKEY_CURRENT_USER, path) - hKey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, path, 0, - _winreg.KEY_ALL_ACCESS) - _winreg.SetValueEx(hKey, "SaveScene", 0, _winreg.REG_DWORD, 1) - _winreg.SetValueEx(hKey, "CustomX", 0, _winreg.REG_DWORD, 1920) - _winreg.SetValueEx(hKey, "CustomY", 0, _winreg.REG_DWORD, 1080) + winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) + hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, + winreg.KEY_ALL_ACCESS) + winreg.SetValueEx(hKey, "SaveScene", 0, winreg.REG_DWORD, 1) + winreg.SetValueEx(hKey, "CustomX", 0, winreg.REG_DWORD, 1920) + winreg.SetValueEx(hKey, "CustomY", 0, winreg.REG_DWORD, 1080) # making sure message dialogs don't appear when overwriting path = r"Software\CelAction\CelAction2D\User Settings\Messages" path += r"\OverwriteScene" - _winreg.CreateKey(_winreg.HKEY_CURRENT_USER, path) - hKey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, path, 0, - _winreg.KEY_ALL_ACCESS) - _winreg.SetValueEx(hKey, "Result", 0, _winreg.REG_DWORD, 6) - _winreg.SetValueEx(hKey, "Valid", 0, _winreg.REG_DWORD, 1) + winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) + hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, + winreg.KEY_ALL_ACCESS) + winreg.SetValueEx(hKey, "Result", 0, winreg.REG_DWORD, 6) + winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1) path = r"Software\CelAction\CelAction2D\User Settings\Messages" path += r"\SceneSaved" - _winreg.CreateKey(_winreg.HKEY_CURRENT_USER, path) - hKey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, path, 0, - _winreg.KEY_ALL_ACCESS) - _winreg.SetValueEx(hKey, "Result", 0, _winreg.REG_DWORD, 1) - _winreg.SetValueEx(hKey, "Valid", 0, _winreg.REG_DWORD, 1) + winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) + hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, + winreg.KEY_ALL_ACCESS) + winreg.SetValueEx(hKey, "Result", 0, winreg.REG_DWORD, 1) + winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1) return True From 7540293c04a02bbab738fcda878a5a1a5e9d351b Mon Sep 17 00:00:00 2001 From: MIlan Kolar Date: Wed, 29 Apr 2020 17:21:13 +0100 Subject: [PATCH 004/131] wip celaction --- pype/hooks/celaction/prelaunch.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/pype/hooks/celaction/prelaunch.py b/pype/hooks/celaction/prelaunch.py index af48be350c..496a7265f2 100644 --- a/pype/hooks/celaction/prelaunch.py +++ b/pype/hooks/celaction/prelaunch.py @@ -26,8 +26,10 @@ class CelactionPrelaunchHook(PypeHook): def execute(self, *args, env: dict = None) -> bool: if not env: env = os.environ + project = env["AVALON_PROJECT"] asset = env["AVALON_ASSET"] task = env["AVALON_TASK"] + app = "pype_publish_standalone" workdir = env["AVALON_WORKDIR"] project_name = f"{asset}_{task}" version = "v001" @@ -52,16 +54,31 @@ class CelactionPrelaunchHook(PypeHook): winreg.KEY_ALL_ACCESS) # TODO: change to root path and pyblish standalone to premiere way - root_path = os.getenv("PIPELINE_ROOT", os.path.dirname(__file__)) - path = os.path.join(root_path, "launchers", "pyblish_standalone.bat") + pype_root_path = os.getenv("PYPE_ROOT") + path = os.path.join(pype_root_path, + "pype.bat") + winreg.SetValueEx(hKey, "SubmitAppTitle", 0, winreg.REG_SZ, path) - parameters = " --path \"*SCENE*\" -d chunk *CHUNK* -d start *START*" - parameters += " -d end *END* -d x *X* -d y *Y* -rh celaction" - parameters += " -8 -d progpath \"*PROGPATH*\"" + parameters = [ + "launch", + f"--app {app}", + f"--project {project}", + f"--asset {asset}", + f"--task {task}", + "--currentFile \"*SCENE*\"", + "--chunk *CHUNK*", + "--frameStart *START*", + "--frameEnd *END*", + "--resolutionWide *X*", + "--resolutionHeight *Y*", + "--registerHost celaction", + "-8", + "--programPath \"\'*PROGPATH*\'\"" + ] winreg.SetValueEx(hKey, "SubmitParametersTitle", 0, winreg.REG_SZ, - parameters) + " ".join(parameters)) # setting resolution parameters path = r"Software\CelAction\CelAction2D\User Settings\Dialogs" From 49746f211c3f4654c51a000b7cd1cc94444a99ca Mon Sep 17 00:00:00 2001 From: MIlan Kolar Date: Wed, 29 Apr 2020 17:27:22 +0100 Subject: [PATCH 005/131] change of celaciton publish name --- pype/hooks/celaction/prelaunch.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pype/hooks/celaction/prelaunch.py b/pype/hooks/celaction/prelaunch.py index 496a7265f2..77d074897d 100644 --- a/pype/hooks/celaction/prelaunch.py +++ b/pype/hooks/celaction/prelaunch.py @@ -29,7 +29,7 @@ class CelactionPrelaunchHook(PypeHook): project = env["AVALON_PROJECT"] asset = env["AVALON_ASSET"] task = env["AVALON_TASK"] - app = "pype_publish_standalone" + app = "celaction_publish" workdir = env["AVALON_WORKDIR"] project_name = f"{asset}_{task}" version = "v001" @@ -74,8 +74,7 @@ class CelactionPrelaunchHook(PypeHook): "--resolutionWide *X*", "--resolutionHeight *Y*", "--registerHost celaction", - "-8", - "--programPath \"\'*PROGPATH*\'\"" + "--programPath \"'*PROGPATH*'\"" ] winreg.SetValueEx(hKey, "SubmitParametersTitle", 0, winreg.REG_SZ, " ".join(parameters)) From d99d99ff024879817c8f5c25bbc7447a2262743b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Apr 2020 18:31:44 +0200 Subject: [PATCH 006/131] feat(celaction): adding custom publishing script --- pype/celaction/publish.py | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 pype/celaction/publish.py diff --git a/pype/celaction/publish.py b/pype/celaction/publish.py new file mode 100644 index 0000000000..2e7d993a60 --- /dev/null +++ b/pype/celaction/publish.py @@ -0,0 +1,57 @@ +import os +import sys +import pype +import importlib +import pyblish.api +import pyblish.util +import avalon.api +from avalon.tools import publish +from pypeapp import Logger + +log = Logger().get_logger(__name__) + + +def main(env): + # Registers pype's Global pyblish plugins + pype.install() + + # Register Host (and it's pyblish plugins) + host_name = env["AVALON_APP"] + # TODO not sure if use "pype." or "avalon." for host import + host_import_str = f"avalon.{host_name}" + + try: + host_module = importlib.import_module(host_import_str) + except ModuleNotFoundError: + log.error(( + f"Host \"{host_name}\" can't be imported." + f" Import string \"{host_import_str}\" failed." + )) + return False + + avalon.api.install(host_module) + + # Register additional paths + addition_paths_str = env.get("PUBLISH_PATHS") or "" + addition_paths = addition_paths_str.split(os.pathsep) + for path in addition_paths: + path = os.path.normpath(path) + if not os.path.exists(path): + continue + + pyblish.api.register_plugin_path(path) + + # Register project specific plugins + project_name = os.environ["AVALON_PROJECT"] + project_plugins_paths = env.get("PYPE_PROJECT_PLUGINS") or "" + for path in project_plugins_paths.split(os.pathsep): + plugin_path = os.path.join(path, project_name, "plugins") + if os.path.exists(plugin_path): + pyblish.api.register_plugin_path(plugin_path) + + return publish.show() + + +if __name__ == "__main__": + result = main(os.environ) + sys.exit(not bool(result)) From 6c979db2cd4848e16e74fc94e5550f2b1bbae383 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Apr 2020 19:51:29 +0200 Subject: [PATCH 007/131] feat(celaction): publishing cli --- pype/celaction/__init__.py | 0 pype/celaction/cli.py | 123 ++++++++++++++++++++++++++++++ pype/hooks/celaction/prelaunch.py | 6 +- 3 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 pype/celaction/__init__.py create mode 100644 pype/celaction/cli.py diff --git a/pype/celaction/__init__.py b/pype/celaction/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pype/celaction/cli.py b/pype/celaction/cli.py new file mode 100644 index 0000000000..d001f0bf24 --- /dev/null +++ b/pype/celaction/cli.py @@ -0,0 +1,123 @@ +import sys +import argparse +import os +import copy +from avalon import io +from pypeapp import execute, Logger + +log = Logger().get_logger("Celaction_cli_publisher") + +publish_host = "celaction" + +CURRENT_DIR = os.path.dirname(__file__) +PACKAGE_DIR = os.path.dirname(CURRENT_DIR) +PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") +PUBLISH_PATH = os.path.join(PLUGINS_DIR, publish_host, "publish") +PUBLISH_SCRIPT_PATH = os.path.join(CURRENT_DIR, "publish.py") + +PUBLISH_PATHS = [ + PUBLISH_PATH, + os.path.join(PLUGINS_DIR, "ftrack", "publish") +] + + +def cli(): + parser = argparse.ArgumentParser(prog="celaction_publish") + + parser.add_argument("--currentFile", + help="Pass file to Context as `currentFile`") + + parser.add_argument("--chunk", + help=("Render chanks on farm")) + + parser.add_argument("--frameStart", + help=("Start of frame range")) + + parser.add_argument("--frameEnd", + help=("End of frame range")) + + parser.add_argument("--resolutionWidth", + help=("Width of resolution")) + + parser.add_argument("--resolutionHeight", + help=("Height of resolution")) + + parser.add_argument("--programDir", + help=("Directory with celaction program installation")) + + return parser.parse_args(sys.argv[1:]).__dict__ + + +def publish(data): + """Triggers publishing script in subprocess. + + """ + try: + publish_env = _prepare_publish_environments( + data + ) + except Exception as exc: + log.warning( + "Failed to prepare environments for publishing.", + exc_info=True + ) + Exception(str(exc)) + + log.info("Pyblish is running") + try: + # Trigger subprocess + # QUESTION should we check returncode? + returncode = execute( + [sys.executable, PUBLISH_SCRIPT_PATH], + env=publish_env + ) + + # Check if output file exists + if returncode != 0: + Exception("Publishing failed") + + log.info("Pyblish have stopped") + + return True + + except Exception: + log.warning("Publishing failed", exc_info=True) + Exception("Publishing failed") + + +def _prepare_publish_environments(data): + """Prepares environments based on request data.""" + env = copy.deepcopy(os.environ) + + project_name = data["project"] + asset_name = data["asset"] + + project_doc = io.find_one({ + "type": "project" + }) + av_asset = io.find_one({ + "type": "asset", + "name": asset_name + }) + parents = av_asset["data"]["parents"] + hierarchy = "" + if parents: + hierarchy = "/".join(parents) + + env["AVALON_PROJECT"] = project_name + env["AVALON_ASSET"] = asset_name + env["AVALON_TASK"] = data["task"] + env["AVALON_WORKDIR"] = data["workdir"] + env["AVALON_HIERARCHY"] = hierarchy + env["AVALON_PROJECTCODE"] = project_doc["data"].get("code", "") + env["AVALON_APP"] = publish_host + env["AVALON_APP_NAME"] = publish_host + + env["PYBLISH_HOSTS"] = publish_host + env["PUBLISH_PATHS"] = os.pathsep.join(PUBLISH_PATHS) + return env + + +if __name__ == "__main__": + data = cli() + publish(data) diff --git a/pype/hooks/celaction/prelaunch.py b/pype/hooks/celaction/prelaunch.py index 77d074897d..98860b5eeb 100644 --- a/pype/hooks/celaction/prelaunch.py +++ b/pype/hooks/celaction/prelaunch.py @@ -56,8 +56,7 @@ class CelactionPrelaunchHook(PypeHook): # TODO: change to root path and pyblish standalone to premiere way pype_root_path = os.getenv("PYPE_ROOT") path = os.path.join(pype_root_path, - "pype.bat") - + "pype.bat") winreg.SetValueEx(hKey, "SubmitAppTitle", 0, winreg.REG_SZ, path) @@ -71,9 +70,8 @@ class CelactionPrelaunchHook(PypeHook): "--chunk *CHUNK*", "--frameStart *START*", "--frameEnd *END*", - "--resolutionWide *X*", + "--resolutionWidth *X*", "--resolutionHeight *Y*", - "--registerHost celaction", "--programPath \"'*PROGPATH*'\"" ] winreg.SetValueEx(hKey, "SubmitParametersTitle", 0, winreg.REG_SZ, From 4de28c180aea622c5e2029a679d6b1910f42d574 Mon Sep 17 00:00:00 2001 From: MIlan Kolar Date: Wed, 29 Apr 2020 19:43:38 +0100 Subject: [PATCH 008/131] fixing celaction publisher --- pype/celaction/__init__.py | 1 + pype/celaction/cli.py | 109 +++++++++++++++++------------- pype/celaction/publish.py | 57 ---------------- pype/hooks/celaction/prelaunch.py | 2 +- 4 files changed, 63 insertions(+), 106 deletions(-) delete mode 100644 pype/celaction/publish.py diff --git a/pype/celaction/__init__.py b/pype/celaction/__init__.py index e69de29bb2..47e81a9212 100644 --- a/pype/celaction/__init__.py +++ b/pype/celaction/__init__.py @@ -0,0 +1 @@ +kwargs = None diff --git a/pype/celaction/cli.py b/pype/celaction/cli.py index d001f0bf24..bbce6247fc 100644 --- a/pype/celaction/cli.py +++ b/pype/celaction/cli.py @@ -1,9 +1,21 @@ -import sys -import argparse import os +import sys import copy +import argparse +import importlib + from avalon import io +import avalon.api +from avalon.tools import publish + +import pyblish.api +import pyblish.util + from pypeapp import execute, Logger +import pype +import pype.celaction + + log = Logger().get_logger("Celaction_cli_publisher") @@ -13,7 +25,6 @@ CURRENT_DIR = os.path.dirname(__file__) PACKAGE_DIR = os.path.dirname(CURRENT_DIR) PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, publish_host, "publish") -PUBLISH_SCRIPT_PATH = os.path.join(CURRENT_DIR, "publish.py") PUBLISH_PATHS = [ PUBLISH_PATH, @@ -45,53 +56,19 @@ def cli(): parser.add_argument("--programDir", help=("Directory with celaction program installation")) - return parser.parse_args(sys.argv[1:]).__dict__ -def publish(data): - """Triggers publishing script in subprocess. - - """ - try: - publish_env = _prepare_publish_environments( - data - ) - except Exception as exc: - log.warning( - "Failed to prepare environments for publishing.", - exc_info=True - ) - Exception(str(exc)) - - log.info("Pyblish is running") - try: - # Trigger subprocess - # QUESTION should we check returncode? - returncode = execute( - [sys.executable, PUBLISH_SCRIPT_PATH], - env=publish_env - ) - - # Check if output file exists - if returncode != 0: - Exception("Publishing failed") - - log.info("Pyblish have stopped") - - return True - - except Exception: - log.warning("Publishing failed", exc_info=True) - Exception("Publishing failed") + pype.celaction.kwargs = parser.parse_args(sys.argv[1:]).__dict__ -def _prepare_publish_environments(data): +def _prepare_publish_environments(): """Prepares environments based on request data.""" env = copy.deepcopy(os.environ) - project_name = data["project"] - asset_name = data["asset"] + project_name = os.getenv("AVALON_PROJECT") + asset_name = os.getenv("AVALON_ASSET") + io.install() project_doc = io.find_one({ "type": "project" }) @@ -106,18 +83,54 @@ def _prepare_publish_environments(data): env["AVALON_PROJECT"] = project_name env["AVALON_ASSET"] = asset_name - env["AVALON_TASK"] = data["task"] - env["AVALON_WORKDIR"] = data["workdir"] + env["AVALON_TASK"] = os.getenv("AVALON_TASK") + env["AVALON_WORKDIR"] = os.getenv("AVALON_WORKDIR") env["AVALON_HIERARCHY"] = hierarchy env["AVALON_PROJECTCODE"] = project_doc["data"].get("code", "") env["AVALON_APP"] = publish_host env["AVALON_APP_NAME"] = publish_host env["PYBLISH_HOSTS"] = publish_host - env["PUBLISH_PATHS"] = os.pathsep.join(PUBLISH_PATHS) - return env + + os.environ.update(env) + +def _main(): + # Registers pype's Global pyblish plugins + pype.install() + + host_import_str = f"pype.{publish_host}" + + try: + host_module = importlib.import_module(host_import_str) + except ModuleNotFoundError: + log.error(( + f"Host \"{publish_host}\" can't be imported." + f" Import string \"{host_import_str}\" failed." + )) + return False + + avalon.api.install(host_module) + + for path in PUBLISH_PATH: + path = os.path.normpath(path) + if not os.path.exists(path): + continue + + pyblish.api.register_plugin_path(path) + + # Register project specific plugins + project_name = os.environ["AVALON_PROJECT"] + project_plugins_paths = os.getenv("PYPE_PROJECT_PLUGINS", "") + for path in project_plugins_paths.split(os.pathsep): + plugin_path = os.path.join(path, project_name, "plugins") + if os.path.exists(plugin_path): + pyblish.api.register_plugin_path(plugin_path) + + return publish.show() if __name__ == "__main__": - data = cli() - publish(data) + cli() + _prepare_publish_environments() + result = _main() + sys.exit(not bool(result)) diff --git a/pype/celaction/publish.py b/pype/celaction/publish.py deleted file mode 100644 index 2e7d993a60..0000000000 --- a/pype/celaction/publish.py +++ /dev/null @@ -1,57 +0,0 @@ -import os -import sys -import pype -import importlib -import pyblish.api -import pyblish.util -import avalon.api -from avalon.tools import publish -from pypeapp import Logger - -log = Logger().get_logger(__name__) - - -def main(env): - # Registers pype's Global pyblish plugins - pype.install() - - # Register Host (and it's pyblish plugins) - host_name = env["AVALON_APP"] - # TODO not sure if use "pype." or "avalon." for host import - host_import_str = f"avalon.{host_name}" - - try: - host_module = importlib.import_module(host_import_str) - except ModuleNotFoundError: - log.error(( - f"Host \"{host_name}\" can't be imported." - f" Import string \"{host_import_str}\" failed." - )) - return False - - avalon.api.install(host_module) - - # Register additional paths - addition_paths_str = env.get("PUBLISH_PATHS") or "" - addition_paths = addition_paths_str.split(os.pathsep) - for path in addition_paths: - path = os.path.normpath(path) - if not os.path.exists(path): - continue - - pyblish.api.register_plugin_path(path) - - # Register project specific plugins - project_name = os.environ["AVALON_PROJECT"] - project_plugins_paths = env.get("PYPE_PROJECT_PLUGINS") or "" - for path in project_plugins_paths.split(os.pathsep): - plugin_path = os.path.join(path, project_name, "plugins") - if os.path.exists(plugin_path): - pyblish.api.register_plugin_path(plugin_path) - - return publish.show() - - -if __name__ == "__main__": - result = main(os.environ) - sys.exit(not bool(result)) diff --git a/pype/hooks/celaction/prelaunch.py b/pype/hooks/celaction/prelaunch.py index 98860b5eeb..a31d54e920 100644 --- a/pype/hooks/celaction/prelaunch.py +++ b/pype/hooks/celaction/prelaunch.py @@ -72,7 +72,7 @@ class CelactionPrelaunchHook(PypeHook): "--frameEnd *END*", "--resolutionWidth *X*", "--resolutionHeight *Y*", - "--programPath \"'*PROGPATH*'\"" + "--programDir \"'*PROGPATH*'\"" ] winreg.SetValueEx(hKey, "SubmitParametersTitle", 0, winreg.REG_SZ, " ".join(parameters)) From 47ecf5d331d606d461dfd03f8616bc4a7aeb8e43 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Apr 2020 20:47:36 +0200 Subject: [PATCH 009/131] fix(celaction): fix code style --- pype/celaction/cli.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pype/celaction/cli.py b/pype/celaction/cli.py index bbce6247fc..499f579fde 100644 --- a/pype/celaction/cli.py +++ b/pype/celaction/cli.py @@ -11,12 +11,11 @@ from avalon.tools import publish import pyblish.api import pyblish.util -from pypeapp import execute, Logger +from pypeapp import Logger import pype import pype.celaction - log = Logger().get_logger("Celaction_cli_publisher") publish_host = "celaction" @@ -56,8 +55,6 @@ def cli(): parser.add_argument("--programDir", help=("Directory with celaction program installation")) - - pype.celaction.kwargs = parser.parse_args(sys.argv[1:]).__dict__ @@ -94,7 +91,11 @@ def _prepare_publish_environments(): os.environ.update(env) -def _main(): + +def main(): + # prepare all environments + _prepare_publish_environments() + # Registers pype's Global pyblish plugins pype.install() @@ -120,7 +121,7 @@ def _main(): # Register project specific plugins project_name = os.environ["AVALON_PROJECT"] - project_plugins_paths = os.getenv("PYPE_PROJECT_PLUGINS", "") + project_plugins_paths = os.getenv("PYPE_PROJECT_PLUGINS", "") for path in project_plugins_paths.split(os.pathsep): plugin_path = os.path.join(path, project_name, "plugins") if os.path.exists(plugin_path): @@ -131,6 +132,5 @@ def _main(): if __name__ == "__main__": cli() - _prepare_publish_environments() - result = _main() + result = main() sys.exit(not bool(result)) From 018dae66353c6453727c50ddc029b3c97a2ef588 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Apr 2020 20:20:45 +0100 Subject: [PATCH 010/131] adding plugins for celaction --- pype/celaction/cli.py | 8 +- .../append_celaction_ftrack_asset_name.py | 19 ++ .../publish/append_celaction_ftrack_data.py | 46 +++ .../celaction/publish/collect_audio.py | 39 +++ .../publish/collect_celaction_render.py | 31 ++ .../publish/collect_celaction_scene.py | 10 + .../plugins/celaction/publish/collect_data.py | 18 ++ .../celaction/publish/collect_kwargs.py | 17 ++ .../publish/extract_celaction_deadline.py | 109 +++++++ .../celaction/publish/integrate_version_up.py | 69 +++++ .../celaction/publish/submit_publish_job.py | 283 ++++++++++++++++++ .../publish/validate_celaction_scene_path.py | 62 ++++ 12 files changed, 708 insertions(+), 3 deletions(-) create mode 100644 pype/plugins/celaction/publish/append_celaction_ftrack_asset_name.py create mode 100644 pype/plugins/celaction/publish/append_celaction_ftrack_data.py create mode 100644 pype/plugins/celaction/publish/collect_audio.py create mode 100644 pype/plugins/celaction/publish/collect_celaction_render.py create mode 100644 pype/plugins/celaction/publish/collect_celaction_scene.py create mode 100644 pype/plugins/celaction/publish/collect_data.py create mode 100644 pype/plugins/celaction/publish/collect_kwargs.py create mode 100644 pype/plugins/celaction/publish/extract_celaction_deadline.py create mode 100644 pype/plugins/celaction/publish/integrate_version_up.py create mode 100644 pype/plugins/celaction/publish/submit_publish_job.py create mode 100644 pype/plugins/celaction/publish/validate_celaction_scene_path.py diff --git a/pype/celaction/cli.py b/pype/celaction/cli.py index 499f579fde..ddbe2ec0c0 100644 --- a/pype/celaction/cli.py +++ b/pype/celaction/cli.py @@ -85,7 +85,7 @@ def _prepare_publish_environments(): env["AVALON_HIERARCHY"] = hierarchy env["AVALON_PROJECTCODE"] = project_doc["data"].get("code", "") env["AVALON_APP"] = publish_host - env["AVALON_APP_NAME"] = publish_host + env["AVALON_APP_NAME"] = "celaction_local" env["PYBLISH_HOSTS"] = publish_host @@ -110,13 +110,15 @@ def main(): )) return False - avalon.api.install(host_module) + # avalon.api.install(host_module) - for path in PUBLISH_PATH: + for path in PUBLISH_PATHS: path = os.path.normpath(path) + if not os.path.exists(path): continue + log.info(f"Registering path: {path}") pyblish.api.register_plugin_path(path) # Register project specific plugins diff --git a/pype/plugins/celaction/publish/append_celaction_ftrack_asset_name.py b/pype/plugins/celaction/publish/append_celaction_ftrack_asset_name.py new file mode 100644 index 0000000000..734bdffe39 --- /dev/null +++ b/pype/plugins/celaction/publish/append_celaction_ftrack_asset_name.py @@ -0,0 +1,19 @@ +import pyblish.api + + +class AppendCelactionFtrackAssetName(pyblish.api.InstancePlugin): + """ Appending "ftrackAssetName" """ + + label = "Ftrack Asset Name" + order = pyblish.api.CollectorOrder + 0.1 + + def process(self, instance): + + # skipping if not launched from ftrack + if "ftrackData" not in instance.context.data: + return + + ftrack_data = instance.context.data["ftrackData"] + + asset_name = ftrack_data["Task"]["name"] + instance.data["ftrackAssetName"] = asset_name diff --git a/pype/plugins/celaction/publish/append_celaction_ftrack_data.py b/pype/plugins/celaction/publish/append_celaction_ftrack_data.py new file mode 100644 index 0000000000..fab3ae8b1a --- /dev/null +++ b/pype/plugins/celaction/publish/append_celaction_ftrack_data.py @@ -0,0 +1,46 @@ +import pyblish.api +from bait.ftrack.query_runner import QueryRunner + + +class AppendCelactionFtrackAudio(pyblish.api.ContextPlugin): + + label = "Ftrack Audio" + order = pyblish.api.ExtractorOrder + + def process(self, context): + + if context.data.get("audio", ''): + self.log.info('Audio data are already collected') + self.log.info('Audio: {}'.format(context.data.get("audio", ''))) + return + + runner = QueryRunner(context.data['ftrackSession']) + + audio_file = runner.get_audio_file_for_shot( + context.data['ftrackData']["Shot"]["id"]) + + if audio_file: + context.data["audio"] = { + 'filename': audio_file, + 'enabled': True + } + else: + self.log.warning("Couldn't find any audio file on Ftrack.") + + +class AppendCelactionFtrackData(pyblish.api.InstancePlugin): + """ Appending ftrack component and asset type data """ + + families = ["img.*", "mov.*"] + # offset to piggy back from default collectors + order = pyblish.api.CollectorOrder + 0.1 + + def process(self, instance): + + # ftrack data + if not instance.context.has_data("ftrackData"): + return + + instance.data["ftrackComponents"] = {} + asset_type = instance.data["family"].split(".")[0] + instance.data["ftrackAssetType"] = asset_type diff --git a/pype/plugins/celaction/publish/collect_audio.py b/pype/plugins/celaction/publish/collect_audio.py new file mode 100644 index 0000000000..bf4e1f47f3 --- /dev/null +++ b/pype/plugins/celaction/publish/collect_audio.py @@ -0,0 +1,39 @@ +import pyblish.api +from bait.paths import get_output_path +import os + + +class AppendCelactionAudio(pyblish.api.ContextPlugin): + + label = "Pype Audio" + order = pyblish.api.CollectorOrder + 0.1 + + def process(self, context): + self.log.info('Collecting Audio Data') + version = context.data('version') if context.has_data('version') else 1 + + task_id = context.data["ftrackData"]["Task"]["id"] + + component_name = context.data["ftrackData"]['Shot']['name'] + version = context.data["version"] + + publish_path = get_output_path( + task_id, component_name, version, "mov").split('/')[0:-4] + + self.log.info('publish_path: {}'.format(publish_path)) + + audio_file = '/'.join(publish_path + [ + 'audio', + 'audioMain', + component_name + '_audioMain_v001.wav' + ]) + + if os.path.exists(audio_file): + context.data["audio"] = { + 'filename': audio_file, + 'enabled': True + } + self.log.info( + 'audio_file: {}, has been added to context'.format(audio_file)) + else: + self.log.warning("Couldn't find any audio file on Ftrack.") diff --git a/pype/plugins/celaction/publish/collect_celaction_render.py b/pype/plugins/celaction/publish/collect_celaction_render.py new file mode 100644 index 0000000000..e6761633f9 --- /dev/null +++ b/pype/plugins/celaction/publish/collect_celaction_render.py @@ -0,0 +1,31 @@ +import os + +import pyblish.api + + +class CollectCelactionRender(pyblish.api.ContextPlugin): + """ Adds the celaction render instances """ + + order = pyblish.api.CollectorOrder + 0.1 + + def process(self, context): + + # scene render + scene_file = os.path.basename(context.data["currentFile"]) + scene_name, _ = os.path.splitext(scene_file) + component_name = scene_name.split(".")[0] + + instance = context.create_instance(name=component_name) + instance.data["family"] = "render" + instance.data["label"] = "{} - remote".format(component_name) + # instance.data["families"] = ["deadline", "remote", "img"] + instance.data["families"] = ["render", "img"] + instance.data["managed_location"] = False + + # getting instance state + instance.data["publish"] = False + + data = context.data("kwargs")["data"] + + for item in data: + instance.set_data(item, value=data[item]) diff --git a/pype/plugins/celaction/publish/collect_celaction_scene.py b/pype/plugins/celaction/publish/collect_celaction_scene.py new file mode 100644 index 0000000000..006edcb1c3 --- /dev/null +++ b/pype/plugins/celaction/publish/collect_celaction_scene.py @@ -0,0 +1,10 @@ +import pyblish.api + + +class CollectCelactionScene(pyblish.api.ContextPlugin): + """ Converts the path flag value to the current file in the context. """ + + order = pyblish.api.CollectorOrder + + def process(self, context): + context.data['ftrackStatus'] = "Ready" diff --git a/pype/plugins/celaction/publish/collect_data.py b/pype/plugins/celaction/publish/collect_data.py new file mode 100644 index 0000000000..ce416403cc --- /dev/null +++ b/pype/plugins/celaction/publish/collect_data.py @@ -0,0 +1,18 @@ +import os +import pyblish.api +import pype.celaction + + +class CollectData(pyblish.api.Collector): + """Collects data passed from via CLI""" + + order = pyblish.api.Collector.order - 0.1 + + def process(self, context): + self.log.info("Adding data from command-line into Context..") + + kwargs = pype.celaction.kwargs.copy() + + for key, value in kwargs.items(): + self.log.info("%s = %s" % (key, value)) + context.set_data(key, value) diff --git a/pype/plugins/celaction/publish/collect_kwargs.py b/pype/plugins/celaction/publish/collect_kwargs.py new file mode 100644 index 0000000000..b2e6d97b8b --- /dev/null +++ b/pype/plugins/celaction/publish/collect_kwargs.py @@ -0,0 +1,17 @@ +import pyblish.api +import pype.celaction + + +class CollectKwargs(pyblish.api.Collector): + """ Collects all keyword arguments passed from the terminal """ + + order = pyblish.api.Collector.order - 0.1 + + def process(self, context): + kwargs = pype.celaction.kwargs.copy() + + self.log.info("Converting nested lists to dict: %s" % kwargs) + kwargs["data"] = dict(kwargs.get("data") or []) + + self.log.info("Storing kwargs: %s" % kwargs) + context.set_data("kwargs", kwargs) diff --git a/pype/plugins/celaction/publish/extract_celaction_deadline.py b/pype/plugins/celaction/publish/extract_celaction_deadline.py new file mode 100644 index 0000000000..3deb612fd5 --- /dev/null +++ b/pype/plugins/celaction/publish/extract_celaction_deadline.py @@ -0,0 +1,109 @@ +import os + +import pyblish.api +import pyblish_standalone +import clique +import requests +from bait.deadline import get_render_settings, get_deadline_data, format_frames +from bait.paths import get_output_path + + +class ExtractCelactionDeadline(pyblish.api.InstancePlugin): + + label = 'Deadline' + families = ['render'] + order = pyblish.api.ExtractorOrder + + def process(self, instance): + + render_settings = get_render_settings("celaction") + + existing_data = instance.data.get( + "deadlineData", {"job": {}, "plugin": {}} + ) + + task_id = instance.context.data["ftrackData"]["Task"]["id"] + + data = get_deadline_data(render_settings, existing_data) + + filename = os.path.basename(instance.context.data["currentFile"]) + filename_no_ext, ext = os.path.splitext(filename) + + data["job"]["Name"] = filename_no_ext + " - " + instance.data["name"] + data["job"]['Frames'] = format_frames( + instance.data('start'), instance.data('end')) + + # get version data + version = instance.context.data( + 'version') if instance.context.has_data('version') else 1 + + output_path = get_output_path( + task_id, instance.data["name"], version, "png") + output_path = output_path.replace("/", "\\") + + data['job']['Plugin'] = 'CelAction' + data['job']["BatchName"] = filename + data['job']["UserName"] = os.environ['USERNAME'] + data["job"]['OutputFilename0'] = output_path.replace('%04d', '####') + + scene_path = pyblish_standalone.kwargs['path'][0] + scene_path = scene_path.replace("/", "\\") + _, ext = os.path.splitext(scene_path) + + # plugin data + self.log.info(scene_path) + + args = '{}'.format(scene_path) + args += ' -a' + args += ' -8' + args += ' -s ' + args += ' -e ' + args += ' -d {}'.format(os.path.dirname(output_path)) + args += ' -x {}'.format(instance.data('x')) + args += ' -y {}'.format(instance.data('y')) + args += ' -r {}'.format(output_path.replace('.%04d', '')) + args += ' -= AbsoluteFrameNumber=on -= PadDigits=4' + args += ' -= ClearAttachment=on' + + data["plugin"]['StartupDirectory'] = '' + data["plugin"]['Arguments'] = args + + self.log.info(data) + + head = output_path.replace('%04d', '') + tail = ".png" + collection = clique.Collection(head=head, padding=4, tail=tail) + + frame_start = int(instance.data['start']) + frame_end = int(instance.data['end']) + + for frame_no in range(frame_start, frame_end): + collection.add(head + str(frame_no).zfill(4) + tail) + + # instance.data["collection"] = collection + + # adding to instance + instance.set_data('deadlineData2', value=data) + # instance.set_data('deadlineSubmissionJob', value=data) + + payload = { + "JobInfo": data["job"], + "PluginInfo": data["plugin"], + "AuxFiles": [] + } + + url = "{}/api/jobs".format('http://192.168.146.8:8082') + response = requests.post(url, json=payload) + if not response.ok: + raise Exception(response.text) + + # Store output dir for unified publisher (filesequence) + instance.data["outputDir"] = os.path.dirname( + data["job"]['OutputFilename0']) + instance.data["deadlineSubmissionJob"] = response.json() + + instance.context.data['ftrackStatus'] = "Render" + + # creating output path + if not os.path.exists(os.path.dirname(output_path)): + os.makedirs(os.path.dirname(output_path)) diff --git a/pype/plugins/celaction/publish/integrate_version_up.py b/pype/plugins/celaction/publish/integrate_version_up.py new file mode 100644 index 0000000000..77c0d7a88b --- /dev/null +++ b/pype/plugins/celaction/publish/integrate_version_up.py @@ -0,0 +1,69 @@ +import shutil +import os +import re +import pyblish.api + + +class VersionUpScene(pyblish.api.ContextPlugin): + order = pyblish.api.IntegratorOrder + label = 'Version Up Scene' + families = ['scene'] + optional = True + active = True + + def process(self, context): + current_file = context.data.get('currentFile') + v_up = get_version_up(current_file) + self.log.debug('Current file is: {}'.format(current_file)) + self.log.debug('Version up: {}'.format(v_up)) + + shutil.copy2(current_file, v_up) + self.log.info('Scene saved into new version: {}'.format(v_up)) + + +def version_get(string, prefix, suffix=None): + """Extract version information from filenames used by DD (and Weta, apparently) + These are _v# or /v# or .v# where v is a prefix string, in our case + we use "v" for render version and "c" for camera track version. + See the version.py and camera.py plugins for usage.""" + + if string is None: + raise ValueError("Empty version string - no match") + + regex = "[/_.]" + prefix + "\d+" + matches = re.findall(regex, string, re.IGNORECASE) + if not len(matches): + msg = "No \"_" + prefix + "#\" found in \"" + string + "\"" + raise ValueError(msg) + return (matches[-1:][0][1], re.search("\d+", matches[-1:][0]).group()) + + +def version_set(string, prefix, oldintval, newintval): + """Changes version information from filenames used by DD (and Weta, apparently) + These are _v# or /v# or .v# where v is a prefix string, in our case + we use "v" for render version and "c" for camera track version. + See the version.py and camera.py plugins for usage.""" + + regex = "[/_.]" + prefix + "\d+" + matches = re.findall(regex, string, re.IGNORECASE) + if not len(matches): + return "" + + # Filter to retain only version strings with matching numbers + matches = filter(lambda s: int(s[2:]) == oldintval, matches) + + # Replace all version strings with matching numbers + for match in matches: + # use expression instead of expr so 0 prefix does not make octal + fmt = "%%(#)0%dd" % (len(match) - 2) + newfullvalue = match[0] + prefix + str(fmt % {"#": newintval}) + string = re.sub(match, newfullvalue, string) + return string + + +def get_version_up(path): + """ Returns the next version of the path """ + + (prefix, v) = version_get(path, 'v') + v = int(v) + return version_set(path, prefix, v, v + 1) diff --git a/pype/plugins/celaction/publish/submit_publish_job.py b/pype/plugins/celaction/publish/submit_publish_job.py new file mode 100644 index 0000000000..8d73dc245f --- /dev/null +++ b/pype/plugins/celaction/publish/submit_publish_job.py @@ -0,0 +1,283 @@ +import os +import json +import pprint +import re +import requests +import pyblish.api + + +class SubmitDependentImageSequenceJobDeadline(pyblish.api.InstancePlugin): + """Submit image sequence publish jobs to Deadline. + + These jobs are dependent on a deadline job submission prior to this + plug-in. + + Renders are submitted to a Deadline Web Service as + supplied via the environment variable DEADLINE_REST_URL + + Options in instance.data: + - deadlineSubmission (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" + + This requires a "startFrame" and "endFrame" to be present in instance.data + or in context.data. + + """ + + label = "Submit image sequence jobs to Deadline" + order = pyblish.api.IntegratorOrder + 0.2 + + hosts = ["celaction"] + + families = [ + "render", + "deadline" + ] + + def process(self, instance): + + # DEADLINE_REST_URL = os.environ.get("DEADLINE_REST_URL", + # "http://localhost:8082") + # assert DEADLINE_REST_URL, "Requires DEADLINE_REST_URL" + + # Get a submission job + + job = instance.data.get("deadlineSubmissionJob") + if not job: + raise RuntimeError("Can't continue without valid deadline " + "submission prior to this plug-in.") + ################ + ft_data = instance.context.data["ftrackData"] + project = ft_data['Project']['name'] + project_code = ft_data['Project']['code'] + projects_path = os.path.dirname(ft_data['Project']['root']) + + data = instance.data.copy() + asset = instance.context.data["ftrackData"]['Shot']['name'] + subset = 'render' + \ + instance.context.data["ftrackData"]['Task']['name'].capitalize() + + state = data.get("publishJobState", "Suspended") + # job_name = "{batch} - {subset} [publish image sequence]".format( + # batch=job["Props"]["Name"], + # subset=subset + # ) + job_name = "{asset} [publish image sequence]".format( + asset=asset + ) + + # Get start/end frame from instance, if not available get from context + context = instance.context + + start = int(instance.data['start']) + end = int(instance.data['end']) + + try: + source = data['source'] + except KeyError: + source = context.data["currentFile"] + + # Write metadata for publish job + render_job = data.pop("deadlineSubmissionJob") + metadata = { + "asset": asset, + "regex": r"^.*\.png", + "subset": subset, + "startFrame": start, + "endFrame": end, + "fps": context.data.get("fps", None), + "families": ["render"], + "source": source, + "user": context.data["user"], + "version": context.data.get('version'), + "audio": context.data["audio"]['filename'], + # Optional metadata (for debugging) + "metadata": { + "instance": data, + "job": job, + "session": fake_avalon_session(project, projects_path) + } + } + + # Ensure output dir exists + output_dir = instance.data["outputDir"] + + if not os.path.isdir(output_dir): + os.makedirs(output_dir) + + for k, v in metadata.items(): + self.log.info(k) + self.log.info(v) + + metadata_filename = "{}_metadata.json".format(subset) + metadata_path = os.path.join(output_dir, metadata_filename) + with open(metadata_path, "w") as f: + json.dump(metadata, f, indent=4, sort_keys=True) + + # Generate the payload for Deadline submission + payload = { + "JobInfo": { + "Plugin": "Python", + "BatchName": job["Props"]["Batch"], + "Name": job_name, + "JobType": "Normal", + "Group": "celaction", + "JobDependency0": job["_id"], + "UserName": os.environ['USERNAME'], + "Comment": instance.context.data.get("comment", ""), + "InitialStatus": "Active" + }, + "PluginInfo": { + "Version": "3.6", + "ScriptFile": r"\\pype\Core\dev\pype-setup\repos\pype-config\pype\scripts\publish_filesequence.py", + "Arguments": '--path "{}"'.format(metadata_path), + "SingleFrameOnly": "True" + }, + + # Mandatory for Deadline, may be empty + "AuxFiles": [] + } + + # Transfer the environment from the original job to this dependent + # job so they use the same environment + environment = fake_env() + environment["AVALON_ASSET"] = asset + environment["AVALON_TASK"] = instance.context.data["ftrackData"]['Task']['name'] + environment["AVALON_PROJECT"] = project + environment["AVALON_PROJECTS"] = projects_path + environment["PYPE_STUDIO_PROJECTS_PUBLISH"] = ft_data['Project']['root'] + environment["PYPE_STUDIO_PROJECTS_RENDER"] = ft_data['Project']['root'] + environment["PYPE_STUDIO_PROJECTS_RESOURCES"] = ft_data['Project']['root'] + environment["PYPE_STUDIO_PROJECTS_WORK"] = ft_data['Project']['root'] + + payload["JobInfo"].update({ + "EnvironmentKeyValue%d" % index: "{key}={value}".format( + key=key, + value=environment[key] + ) for index, key in enumerate(environment) + }) + + # Avoid copied pools and remove secondary pool + payload["JobInfo"]["Pool"] = "animation_2d" + payload["JobInfo"].pop("SecondaryPool", None) + + self.log.info("Submitting..") + # self.log.info(json.dumps(payload, indent=4, sort_keys=True)) + + ################ + ###################### + fake_instance = instance.context.create_instance( + name=(str(instance) + "1")) + + for k, v in data.items(): + self.log.info(k) + fake_instance.data[k] = v + + # fake_instance.data['deadlineData'] = payload + # 'http://192.168.146.8:8082' + url = "{}/api/jobs".format('http://192.168.146.8:8082') + response = requests.post(url, json=payload) + if not response.ok: + raise Exception(response.text) + ###################### + ####################### + + +def fake_avalon_session(project=None, projects_path=None): + return { + "AVALON_APP": "premiere", + "AVALON_APP_VERSION": "2019", + "AVALON_ASSET": "editorial", + "AVALON_CONFIG": "pype", + "AVALON_CONTAINER_ID": "avalon.container", + "AVALON_DB": "Pype", + "AVALON_DEADLINE": "http://192.168.146.8:8082", + "AVALON_DEBUG": "1", + "AVALON_EARLY_ADOPTER": "1", + "AVALON_INSTANCE_ID": "avalon.instance", + "AVALON_LABEL": "Avalon", + "AVALON_LOCATION": "http://127.0.0.1", + "AVALON_MONGO": "mongodb://PypeAdmin:X34vkuwL4wbK9A7X@192.168.146.24:27072/Pype", + "AVALON_PASSWORD": "secret", + "AVALON_PROJECT": project or "LBB2_dev", + "AVALON_PROJECTS": projects_path or "L:/PYPE_test", + "AVALON_SILO": "editorial", + "AVALON_TASK": "conform", + "AVALON_TIMEOUT": "1000", + "AVALON_USERNAME": "avalon", + "AVALON_WORKDIR": "L:/PYPE_test/episodes/editorial/work/conform", + "schema": "avalon-core:session-1.0" + } + + +def fake_env(): + return { + "AVALON_CONFIG": "pype", + "AVALON_CONTAINER_ID": "avalon.container", + "AVALON_CORE": "\\\\pype\\Core\\dev\\pype-setup\\repos\\avalon-core", + "AVALON_DB": "Pype", + "AVALON_DB_DATA": "\\\\pype\\Core\\dev\\mongo_db_data", + "AVALON_DEADLINE": "http://192.168.146.8:8082", + "AVALON_DEBUG": "1", + "AVALON_EARLY_ADOPTER": "1", + "AVALON_ENV_NAME": "pype_env", + "AVALON_HIERARCHY": "", + "AVALON_INSTANCE_ID": "avalon.instance", + "AVALON_LABEL": "Avalon", + "AVALON_LAUNCHER": "\\\\pype\\Core\\dev\\pype-setup\\repos\\avalon-launcher", + "AVALON_LOCATION": "http://127.0.0.1", + "AVALON_MONGO": "mongodb://PypeAdmin:X34vkuwL4wbK9A7X@192.168.146.24:27072/Pype", + "AVALON_MONGO_PORT": "27072", + "AVALON_PASSWORD": "secret", + "AVALON_SCHEMA": "\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates\\schema", + "AVALON_SILO": "", + "AVALON_TIMEOUT": "1000", + "AVALON_USERNAME": "avalon", + "AVALON_WORKDIR": "default", + "DEADLINE_PATH": "C:\\Program Files\\Thinkbox\\Deadline10\\bin", + "DEADLINE_REST_URL": "http://192.168.146.8:8082", + "FTRACK_API_KEY": "NGI0ZGU3ZjMtNzNiZC00NGVlLWEwY2EtMzA1OWJlZGM0MjAyOjozZWZmMThjZi04MjkwLTQxMzQtODUwMC03NTZhMGJiZTM2MTA", + "FTRACK_API_USER": "license@clothcatanimation.com", + "FTRACK_SERVER": "https://clothcat2.ftrackapp.com", + "MONGO_DB_ENTRYDB": "Pype", + "MONGO_DB_PASS": "X34vkuwL4wbK9A7X", + "MONGO_DB_USER": "PypeAdmin", + "PATH": "\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\Scripts;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\Library\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\Library;;\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates\\bin;\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates\\bin\\windows;\\\\pype\\Core\\dev\\pype-setup\\app;\\\\pype\\core\\software\\ffmpeg\\bin;\\\\pype\\Core\\dev\\Applications\\djv\\bin", + "PYBLISHPLUGINPATH": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pype-config\\pype\\plugins\\ftrack\\publish;", + "PYBLISH_BASE": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-base", + "PYBLISH_HOSTS": "shell", + "PYBLISH_LITE": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-lite", + "PYBLISH_QML": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-qml", + "PYPE_APP_ROOT": "\\\\pype\\Core\\dev\\pype-setup\\app", + "PYPE_DEBUG": "3", + "PYPE_DEBUG_STDOUT": "0", + "PYPE_SETUP_ROOT": "\\\\pype\\Core\\dev\\pype-setup", + "PYPE_STUDIO_CODE": "CC", + "PYPE_STUDIO_CONFIG": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pype-config", + "PYPE_STUDIO_CORE": "\\\\pype\\Core\\dev", + "PYPE_STUDIO_CORE_MOUNT": "\\\\pype\\Core\\dev", + "PYPE_STUDIO_NAME": "Cloth Cat", + "PYPE_STUDIO_SOFTWARE": "\\\\pype\\Core\\dev\\Applications", + "PYPE_STUDIO_TEMPLATES": "\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates", + "PYPE_STUDIO_TOOLS": "\\\\pype\\Core\\dev\\production\\tools", + "PYTHONPATH": "\\\\pype\\Core\\dev\\pype-setup;\\\\pype\\Core\\dev\\pype-setup\\app\\vendor;\\\\pype\\Core\\dev\\pype-setup\\repos\\avalon-core;\\\\pype\\Core\\dev\\pype-setup\\repos\\avalon-launcher;\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-base;\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-qml;\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-lite;\\\\pype\\Core\\dev\\pype-setup\\repos\\ftrack-event-server;\\\\pype\\Core\\dev\\pype-setup\\repos\\pype-config;\\\\pype\\Core\\dev\\pype-setup\\app\\vendor;\\\\pype\\Core\\dev\\pype-setup\\repos\\pype-config\\pype\\vendor;\\\\pype\\Core\\dev\\pype-setup\\repos\\ftrack-event-server", + "PYTHONVERBOSE": "True", + "PYTHON_ENV": "C:\\Users\\Public\\pype_env", + "REMOTE_ENV_DIR": "\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env", + "REMOTE_ENV_ON": "0", + "SCHEMA": "avalon-core:session-1.0", + "STUDIO_SOFT": "\\\\evo2\\core\\Applications", + "TOOL_ENV": "\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates\\environments", + "USERNAME": "pype" + } diff --git a/pype/plugins/celaction/publish/validate_celaction_scene_path.py b/pype/plugins/celaction/publish/validate_celaction_scene_path.py new file mode 100644 index 0000000000..a73819b9ca --- /dev/null +++ b/pype/plugins/celaction/publish/validate_celaction_scene_path.py @@ -0,0 +1,62 @@ +import shutil +import pyblish.api +import pyblish_standalone +import os +from bait.paths import get_env_work_file + + +class RepairCelactionScenePath(pyblish.api.Action): + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + + # get version data + version = context.data('version') if context.has_data('version') else 1 + + task_id = context.data["ftrackData"]["Task"]["id"] + expected_path = get_env_work_file( + "celaction", task_id, "scn", version).replace('\\\\\\', '\\\\') + + src = context.data["currentFile"] + + if not os.path.exists(os.path.dirname(expected_path)): + os.makedirs(os.path.dirname(expected_path)) + + if os.path.exists(os.path.dirname(expected_path)): + self.log.info("existing to \"%s\"" % expected_path) + + if os.path.exists(expected_path) and ('v001' in expected_path): + os.remove(expected_path) + + shutil.copy2(src, expected_path) + + pyblish_standalone.kwargs['path'] = [expected_path] + context.data["currentFile"] = expected_path + + self.log.info("Saved to \"%s\"" % expected_path) + + +class ValidateCelactionScenePath(pyblish.api.InstancePlugin): + order = pyblish.api.ValidatorOrder + families = ['scene'] + label = 'Scene Path' + actions = [RepairCelactionScenePath] + + def process(self, instance): + + # getting current work file + current_scene_path = pyblish_standalone.kwargs['path'][0] + + version = instance.context.data( + 'version') if instance.context.has_data('version') else 1 + + task_id = instance.context.data["ftrackData"]["Task"]["id"] + expected_scene_path = get_env_work_file( + "celaction", task_id, "scn", version).replace('\\\\\\', '\\\\') + + msg = 'Scene path is not correct: Current: {}, Expected: {}'.format( + current_scene_path, expected_scene_path) + + assert expected_scene_path == current_scene_path, msg From 08bc7a533baa571e09c68485703124e64646be9c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 30 Apr 2020 13:23:58 +0200 Subject: [PATCH 011/131] feat(celaction): prelaunch hool with Anatomy last version workfile --- pype/api.py | 2 + pype/celaction/cli.py | 13 ---- pype/hooks/celaction/prelaunch.py | 101 ++++++++++++++++++++++++++---- pype/lib.py | 34 ++++++++++ 4 files changed, 125 insertions(+), 25 deletions(-) diff --git a/pype/api.py b/pype/api.py index 2c227b5b4b..045cb260ee 100644 --- a/pype/api.py +++ b/pype/api.py @@ -25,6 +25,7 @@ from .lib import ( get_hierarchy, get_subsets, get_version_from_path, + get_last_version_from_path, modified_environ, add_tool_to_environment ) @@ -56,6 +57,7 @@ __all__ = [ "get_asset", "get_subsets", "get_version_from_path", + "get_last_version_from_path", "modified_environ", "add_tool_to_environment", diff --git a/pype/celaction/cli.py b/pype/celaction/cli.py index ddbe2ec0c0..f6d518a5a9 100644 --- a/pype/celaction/cli.py +++ b/pype/celaction/cli.py @@ -99,19 +99,6 @@ def main(): # Registers pype's Global pyblish plugins pype.install() - host_import_str = f"pype.{publish_host}" - - try: - host_module = importlib.import_module(host_import_str) - except ModuleNotFoundError: - log.error(( - f"Host \"{publish_host}\" can't be imported." - f" Import string \"{host_import_str}\" failed." - )) - return False - - # avalon.api.install(host_module) - for path in PUBLISH_PATHS: path = os.path.normpath(path) diff --git a/pype/hooks/celaction/prelaunch.py b/pype/hooks/celaction/prelaunch.py index a31d54e920..9af3cdd740 100644 --- a/pype/hooks/celaction/prelaunch.py +++ b/pype/hooks/celaction/prelaunch.py @@ -2,7 +2,10 @@ import logging import os import winreg from pype.lib import PypeHook -from pypeapp import Logger +from pype.api import get_last_version_from_path +from pypeapp import Anatomy, Logger + +from avalon import io, api, lib log = logging.getLogger(__name__) @@ -14,6 +17,7 @@ class CelactionPrelaunchHook(PypeHook): path to the project by environment variable to Unreal launcher shell script. """ + workfile_ext = "scn" def __init__(self, logger=None): if not logger: @@ -25,21 +29,33 @@ class CelactionPrelaunchHook(PypeHook): def execute(self, *args, env: dict = None) -> bool: if not env: - env = os.environ - project = env["AVALON_PROJECT"] - asset = env["AVALON_ASSET"] - task = env["AVALON_TASK"] - app = "celaction_publish" - workdir = env["AVALON_WORKDIR"] - project_name = f"{asset}_{task}" - version = "v001" + self.env = os.environ + else: + self.env = env - self.log.info(f"{self.signature}") + self._S = api.Session + project = self._S["AVALON_PROJECT"] = self.env["AVALON_PROJECT"] + asset = self._S["AVALON_ASSET"] = self.env["AVALON_ASSET"] + task = self._S["AVALON_TASK"] = self.env["AVALON_TASK"] + workdir = self._S["AVALON_WORKDIR"] = self.env["AVALON_WORKDIR"] + + anatomy_filled = self.get_anatomy_filled() + + app = "celaction_publish" + workfile = anatomy_filled["work"]["file"] + version = anatomy_filled["version"] os.makedirs(workdir, exist_ok=True) self.log.info(f"Work dir is: `{workdir}`") - project_file = os.path.join(workdir, f"{project_name}_{version}.scn") + # get last version if any + workfile_last = get_last_version_from_path( + workdir, workfile.split(version)) + + if workfile_last: + workfile = workfile_last + + project_file = os.path.join(workdir, workfile) env["PYPE_CELACTION_PROJECT_FILE"] = project_file self.log.info(f"Workfile is: `{project_file}`") @@ -73,7 +89,7 @@ class CelactionPrelaunchHook(PypeHook): "--resolutionWidth *X*", "--resolutionHeight *Y*", "--programDir \"'*PROGPATH*'\"" - ] + ] winreg.SetValueEx(hKey, "SubmitParametersTitle", 0, winreg.REG_SZ, " ".join(parameters)) @@ -105,3 +121,64 @@ class CelactionPrelaunchHook(PypeHook): winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1) return True + + def get_anatomy_filled(self): + root_path = api.registered_root() + project_name = self._S["AVALON_PROJECT"] + asset_name = self._S["AVALON_ASSET"] + + io.install() + project_entity = io.find_one({ + "type": "project", + "name": project_name + }) + assert project_entity, ( + "Project '{0}' was not found." + ).format(project_name) + log.debug("Collected Project \"{}\"".format(project_entity)) + + asset_entity = io.find_one({ + "type": "asset", + "name": asset_name, + "parent": project_entity["_id"] + }) + assert asset_entity, ( + "No asset found by the name '{0}' in project '{1}'" + ).format(asset_name, project_name) + + project_name = project_entity["name"] + + log.info( + "Anatomy object collected for project \"{}\".".format(project_name) + ) + + hierarchy_items = asset_entity["data"]["parents"] + hierarchy = "" + if hierarchy_items: + hierarchy = os.path.join(*hierarchy_items) + + template_data = { + "root": root_path, + "project": { + "name": project_name, + "code": project_entity["data"].get("code") + }, + "asset": asset_entity["name"], + "hierarchy": hierarchy.replace("\\", "/"), + "task": self._S["AVALON_TASK"], + "ext": self.workfile_ext, + "version": 1, + "username": os.getenv("PYPE_USERNAME", "").strip() + } + + avalon_app_name = os.environ.get("AVALON_APP_NAME") + if avalon_app_name: + application_def = lib.get_application(avalon_app_name) + app_dir = application_def.get("application_dir") + if app_dir: + template_data["app"] = app_dir + + anatomy = Anatomy(project_name) + anatomy_filled = anatomy.format_all(template_data).get_solved() + + return anatomy_filled diff --git a/pype/lib.py b/pype/lib.py index d3ccbc8589..2bd18dacff 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -469,6 +469,40 @@ def get_version_from_path(file): ) +def get_last_version_from_path(path_dir, filter=None): + """ + Finds last version of given directory content + + Args: + path_dir (string): directory path + filter (list): list of strings used as file name filter + + Returns: + string: file name with last version + + """ + assert os.path.isdir(path_dir), "`path_dir` argument needs to be directory" + + filtred_files = list() + + # form regex for filtering + patern = r".*" + + if filter: + patern = patern.join(filter) + + for f in os.listdir(path_dir): + if not re.findall(patern, f): + continue + filtred_files.append(f) + + if filtred_files: + sorted(filtred_files) + return filtred_files[-1] + else: + return None + + def get_avalon_database(): if io._database is None: set_io_database() From bbb4240598fdd590ae0c1d2f241708c629b55d32 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 30 Apr 2020 15:30:37 +0200 Subject: [PATCH 012/131] feat(celaction): wip support deadline submition --- .../celaction/publish/collect_render_path.py | 60 ++++ ...e.py => extract_celaction_deadline_old.py} | 218 ++++++------- .../publish/submit_celaction_deadline.py | 287 ++++++++++++++++++ 3 files changed, 456 insertions(+), 109 deletions(-) create mode 100644 pype/plugins/celaction/publish/collect_render_path.py rename pype/plugins/celaction/publish/{extract_celaction_deadline.py => extract_celaction_deadline_old.py} (95%) create mode 100644 pype/plugins/celaction/publish/submit_celaction_deadline.py diff --git a/pype/plugins/celaction/publish/collect_render_path.py b/pype/plugins/celaction/publish/collect_render_path.py new file mode 100644 index 0000000000..72adf57e86 --- /dev/null +++ b/pype/plugins/celaction/publish/collect_render_path.py @@ -0,0 +1,60 @@ +""" +Requires: + context -> anatomy + context -> anatomyData + +Provides: + instance -> publishDir + instance -> resourcesDir +""" + +import os +import copy + +import pyblish.api +from avalon import api + + +class CollectRenderPath(pyblish.api.InstancePlugin): + """Generate file and directory path where rendered images will be""" + + label = "Collect Render Path" + order = pyblish.api.CollectorOrder + 0.495 + + def process(self, instance): + anatomy = instance.context.data["anatomy"] + + template_data = copy.deepcopy(instance.data["anatomyData"]) + + # This is for cases of Deprecated anatomy without `folder` + # TODO remove when all clients have solved this issue + template_data.update({ + "frame": "FRAME_TEMP", + "representation": "png" + }) + + anatomy_filled = anatomy.format(template_data) + + if "folder" in anatomy.templates["render"]: + render_folder = anatomy_filled["render"]["folder"] + render_file = anatomy_filled["render"]["file"] + else: + # solve deprecated situation when `folder` key is not underneath + # `publish` anatomy + project_name = api.Session["AVALON_PROJECT"] + self.log.warning(( + "Deprecation warning: Anatomy does not have set `folder`" + " key underneath `publish` (in global of for project `{}`)." + ).format(project_name)) + + file_path = anatomy_filled["render"]["path"] + # Directory + render_folder = os.path.dirname(file_path) + render_file = os.path.basename(file_path) + + render_folder = os.path.normpath(render_folder) + render_path = os.path.join(render_folder, render_file) + + instance.data["outputRenderPath"] = render_path + + self.log.debug("outputRenderPath: \"{}\"".format(render_path)) diff --git a/pype/plugins/celaction/publish/extract_celaction_deadline.py b/pype/plugins/celaction/publish/extract_celaction_deadline_old.py similarity index 95% rename from pype/plugins/celaction/publish/extract_celaction_deadline.py rename to pype/plugins/celaction/publish/extract_celaction_deadline_old.py index 3deb612fd5..322bb468f9 100644 --- a/pype/plugins/celaction/publish/extract_celaction_deadline.py +++ b/pype/plugins/celaction/publish/extract_celaction_deadline_old.py @@ -1,109 +1,109 @@ -import os - -import pyblish.api -import pyblish_standalone -import clique -import requests -from bait.deadline import get_render_settings, get_deadline_data, format_frames -from bait.paths import get_output_path - - -class ExtractCelactionDeadline(pyblish.api.InstancePlugin): - - label = 'Deadline' - families = ['render'] - order = pyblish.api.ExtractorOrder - - def process(self, instance): - - render_settings = get_render_settings("celaction") - - existing_data = instance.data.get( - "deadlineData", {"job": {}, "plugin": {}} - ) - - task_id = instance.context.data["ftrackData"]["Task"]["id"] - - data = get_deadline_data(render_settings, existing_data) - - filename = os.path.basename(instance.context.data["currentFile"]) - filename_no_ext, ext = os.path.splitext(filename) - - data["job"]["Name"] = filename_no_ext + " - " + instance.data["name"] - data["job"]['Frames'] = format_frames( - instance.data('start'), instance.data('end')) - - # get version data - version = instance.context.data( - 'version') if instance.context.has_data('version') else 1 - - output_path = get_output_path( - task_id, instance.data["name"], version, "png") - output_path = output_path.replace("/", "\\") - - data['job']['Plugin'] = 'CelAction' - data['job']["BatchName"] = filename - data['job']["UserName"] = os.environ['USERNAME'] - data["job"]['OutputFilename0'] = output_path.replace('%04d', '####') - - scene_path = pyblish_standalone.kwargs['path'][0] - scene_path = scene_path.replace("/", "\\") - _, ext = os.path.splitext(scene_path) - - # plugin data - self.log.info(scene_path) - - args = '{}'.format(scene_path) - args += ' -a' - args += ' -8' - args += ' -s ' - args += ' -e ' - args += ' -d {}'.format(os.path.dirname(output_path)) - args += ' -x {}'.format(instance.data('x')) - args += ' -y {}'.format(instance.data('y')) - args += ' -r {}'.format(output_path.replace('.%04d', '')) - args += ' -= AbsoluteFrameNumber=on -= PadDigits=4' - args += ' -= ClearAttachment=on' - - data["plugin"]['StartupDirectory'] = '' - data["plugin"]['Arguments'] = args - - self.log.info(data) - - head = output_path.replace('%04d', '') - tail = ".png" - collection = clique.Collection(head=head, padding=4, tail=tail) - - frame_start = int(instance.data['start']) - frame_end = int(instance.data['end']) - - for frame_no in range(frame_start, frame_end): - collection.add(head + str(frame_no).zfill(4) + tail) - - # instance.data["collection"] = collection - - # adding to instance - instance.set_data('deadlineData2', value=data) - # instance.set_data('deadlineSubmissionJob', value=data) - - payload = { - "JobInfo": data["job"], - "PluginInfo": data["plugin"], - "AuxFiles": [] - } - - url = "{}/api/jobs".format('http://192.168.146.8:8082') - response = requests.post(url, json=payload) - if not response.ok: - raise Exception(response.text) - - # Store output dir for unified publisher (filesequence) - instance.data["outputDir"] = os.path.dirname( - data["job"]['OutputFilename0']) - instance.data["deadlineSubmissionJob"] = response.json() - - instance.context.data['ftrackStatus'] = "Render" - - # creating output path - if not os.path.exists(os.path.dirname(output_path)): - os.makedirs(os.path.dirname(output_path)) +import os + +import pyblish.api +import pyblish_standalone +import clique +import requests +from bait.deadline import get_render_settings, get_deadline_data, format_frames +from bait.paths import get_output_path + + +class ExtractCelactionDeadline(pyblish.api. InstancePlugin): + + label = 'Deadline' + families = ['render'] + order = pyblish.api.ExtractorOrder + + def process(self, instance): + + render_settings = get_render_settings("celaction") + + existing_data = instance.data.get( + "deadlineData", {"job": {}, "plugin": {}} + ) + + task_id = instance.context.data["ftrackData"]["Task"]["id"] + + data = get_deadline_data(render_settings, existing_data) + + filename = os.path.basename(instance.context.data["currentFile"]) + filename_no_ext, ext = os.path.splitext(filename) + + data["job"]["Name"] = filename_no_ext + " - " + instance.data["name"] + data["job"]['Frames'] = format_frames( + instance.data('start'), instance.data('end')) + + # get version data + version = instance.context.data( + 'version') if instance.context.has_data('version') else 1 + + output_path = get_output_path( + task_id, instance.data["name"], version, "png") + output_path = output_path.replace("/", "\\") + + data['job']['Plugin'] = 'CelAction' + data['job']["BatchName"] = filename + data['job']["UserName"] = os.environ['USERNAME'] + data["job"]['OutputFilename0'] = output_path.replace('%04d', '####') + + scene_path = pyblish_standalone.kwargs['path'][0] + scene_path = scene_path.replace("/", "\\") + _, ext = os.path.splitext(scene_path) + + # plugin data + self.log.info(scene_path) + + args = '{}'.format(scene_path) + args += ' -a' + args += ' -8' + args += ' -s ' + args += ' -e ' + args += ' -d {}'.format(os.path.dirname(output_path)) + args += ' -x {}'.format(instance.data('x')) + args += ' -y {}'.format(instance.data('y')) + args += ' -r {}'.format(output_path.replace('.%04d', '')) + args += ' -= AbsoluteFrameNumber=on -= PadDigits=4' + args += ' -= ClearAttachment=on' + + data["plugin"]['StartupDirectory'] = '' + data["plugin"]['Arguments'] = args + + self.log.info(data) + + head = output_path.replace('%04d', '') + tail = ".png" + collection = clique.Collection(head=head, padding=4, tail=tail) + + frame_start = int(instance.data['start']) + frame_end = int(instance.data['end']) + + for frame_no in range(frame_start, frame_end): + collection.add(head + str(frame_no).zfill(4) + tail) + + # instance.data["collection"] = collection + + # adding to instance + instance.set_data('deadlineData2', value=data) + # instance.set_data('deadlineSubmissionJob', value=data) + + payload = { + "JobInfo": data["job"], + "PluginInfo": data["plugin"], + "AuxFiles": [] + } + + url = "{}/api/jobs".format('http://192.168.146.8:8082') + response = requests.post(url, json=payload) + if not response.ok: + raise Exception(response.text) + + # Store output dir for unified publisher (filesequence) + instance.data["outputDir"] = os.path.dirname( + data["job"]['OutputFilename0']) + instance.data["deadlineSubmissionJob"] = response.json() + + instance.context.data['ftrackStatus'] = "Render" + + # creating output path + if not os.path.exists(os.path.dirname(output_path)): + os.makedirs(os.path.dirname(output_path)) diff --git a/pype/plugins/celaction/publish/submit_celaction_deadline.py b/pype/plugins/celaction/publish/submit_celaction_deadline.py new file mode 100644 index 0000000000..3ea9b929e3 --- /dev/null +++ b/pype/plugins/celaction/publish/submit_celaction_deadline.py @@ -0,0 +1,287 @@ +import os +import json +import getpass + +from avalon import api +from avalon.vendor import requests +import re +import pyblish.api + + +class ExtractCelactionDeadline(pyblish.api.InstancePlugin): + """Submit CelAction2D scene to Deadline + + Renders are submitted to a Deadline Web Service as + supplied via the environment variable DEADLINE_REST_URL + + """ + + label = "Submit CelAction to Deadline" + order = pyblish.api.IntegratorOrder + 0.1 + hosts = ["celaction"] + families = ["render"] + + deadline_department = "" + deadline_priority = 50 + deadline_pool = "" + deadline_pool_secondary = "" + deadline_group = "" + deadline_chunk_size = 1 + + def process(self, instance): + families = instance.data["families"] + + context = instance.context + + DEADLINE_REST_URL = os.environ.get("DEADLINE_REST_URL", + "http://localhost:8082") + assert DEADLINE_REST_URL, "Requires DEADLINE_REST_URL" + + self.deadline_url = "{}/api/jobs".format(DEADLINE_REST_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()) + self._frame_start = int(instance.data["frameStartHandle"]) + self._frame_end = int(instance.data["frameEndHandle"]) + + # 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"] = families.insert(0, "render2d") + + def payload_submit(self, + instance, + script_path, + render_path, + responce_data=None + ): + render_dir = os.path.normpath(os.path.dirname(render_path)) + script_name = os.path.basename(script_path) + jobname = "%s - %s" % (script_name, instance.name) + + output_filename_0 = self.preview_fname(render_path) + + if not responce_data: + responce_data = {} + + try: + # Ensure render folder exists + os.makedirs(render_dir) + except OSError: + pass + + # define chunk and priority + chunk_size = instance.data.get("deadlineChunkSize") + if chunk_size == 0: + chunk_size = self.deadline_chunk_size + + payload = { + "JobInfo": { + # Top-level group name + "BatchName": script_name, + + # Job name, as seen in Monitor + "Name": jobname, + + # Arbitrary username, for visualisation in Monitor + "UserName": self._deadline_user, + + "Department": self.deadline_department, + "Priority": self.deadline_priority, + "ChunkSize": chunk_size, + + "Group": self.deadline_group, + "Pool": self.deadline_pool, + "SecondaryPool": self.deadline_pool_secondary, + + "Plugin": "CelAction", + "Frames": "{start}-{end}".format( + start=self._frame_start, + end=self._frame_end + ), + "Comment": self._comment, + + # Optional, enable double-click to preview rendered + # frames from Deadline Monitor + "OutputFilename0": output_filename_0.replace("\\", "/") + + }, + "PluginInfo": { + # Input + "SceneFile": script_path, + + # Output directory and filename + "OutputFilePath": render_dir.replace("\\", "/"), + + # Mandatory for Deadline + "Version": self._ver.group(), + + # Resolve relative references + "ProjectPath": script_path, + "AWSAssetFile0": render_path, + }, + + # Mandatory for Deadline, may be empty + "AuxFiles": [] + } + + if responce_data.get("_id"): + payload["JobInfo"].update({ + "JobType": "Normal", + "BatchName": responce_data["Props"]["Batch"], + "JobDependency0": responce_data["_id"], + "ChunkSize": 99999999 + }) + + # Include critical environment variables with submission + keys = [ + "PYTHONPATH", + "PATH", + "AVALON_SCHEMA", + "PYBLISHPLUGINPATH", + "TOOL_ENV" + ] + environment = dict({key: os.environ[key] for key in keys + if key in os.environ}, **api.Session) + + for path in os.environ: + if path.lower().startswith('pype_'): + environment[path] = os.environ[path] + + environment["PATH"] = os.environ["PATH"] + clean_environment = {} + for key in environment: + clean_path = "" + self.log.debug("key: {}".format(key)) + to_process = environment[key] + if key == "PYPE_STUDIO_CORE_MOUNT": + clean_path = environment[key] + elif "://" in environment[key]: + clean_path = environment[key] + elif os.pathsep not in to_process: + try: + path = environment[key] + path.decode('UTF-8', 'strict') + clean_path = os.path.normpath(path) + except UnicodeDecodeError: + print('path contains non UTF characters') + else: + for path in environment[key].split(os.pathsep): + try: + path.decode('UTF-8', 'strict') + clean_path += os.path.normpath(path) + os.pathsep + except UnicodeDecodeError: + print('path contains non UTF characters') + + if key == "PYTHONPATH": + clean_path = clean_path.replace('python2', 'python3') + + clean_path = clean_path.replace( + os.path.normpath( + environment['PYPE_STUDIO_CORE_MOUNT']), # noqa + os.path.normpath( + environment['PYPE_STUDIO_CORE_PATH'])) # noqa + clean_environment[key] = clean_path + + environment = clean_environment + + 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.info("using render plugin : {}".format(plugin)) + + self.log.info("Submitting..") + self.log.info(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"])) + response = requests.post(self.deadline_url, json=payload) + + 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 + else: + return path + + def expected_files(self, + instance, + path): + """ Create expected files in instance data + """ + if not instance.data.get("expectedFiles"): + instance.data["expectedFiles"] = list() + + dir = os.path.dirname(path) + file = os.path.basename(path) + + if "#" in file: + pparts = file.split("#") + padding = "%0{}d".format(len(pparts) - 1) + file = pparts[0] + padding + pparts[-1] + + if "%" not in file: + instance.data["expectedFiles"].append(path) + return + + for i in range(self._frame_start, (self._frame_end + 1)): + instance.data["expectedFiles"].append( + os.path.join(dir, (file % i)).replace("\\", "/")) From 1faec702ec21c77bcbce9b420dea461bda7a08cd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 30 Apr 2020 15:50:10 +0200 Subject: [PATCH 013/131] clean(celaction): removing old plugins --- .../append_celaction_ftrack_asset_name.py | 38 +- .../append_celaction_ftrack_data.py | 92 +-- .../extract_celaction_deadline_old.py | 0 .../submit_publish_job.py | 566 +++++++++--------- .../validate_celaction_scene_path.py | 124 ++-- 5 files changed, 410 insertions(+), 410 deletions(-) rename pype/plugins/celaction/{publish => _unused_publish}/append_celaction_ftrack_asset_name.py (96%) rename pype/plugins/celaction/{publish => _unused_publish}/append_celaction_ftrack_data.py (96%) rename pype/plugins/celaction/{publish => _unused_publish}/extract_celaction_deadline_old.py (100%) rename pype/plugins/celaction/{publish => _unused_publish}/submit_publish_job.py (97%) rename pype/plugins/celaction/{publish => _unused_publish}/validate_celaction_scene_path.py (96%) diff --git a/pype/plugins/celaction/publish/append_celaction_ftrack_asset_name.py b/pype/plugins/celaction/_unused_publish/append_celaction_ftrack_asset_name.py similarity index 96% rename from pype/plugins/celaction/publish/append_celaction_ftrack_asset_name.py rename to pype/plugins/celaction/_unused_publish/append_celaction_ftrack_asset_name.py index 734bdffe39..175a19edd3 100644 --- a/pype/plugins/celaction/publish/append_celaction_ftrack_asset_name.py +++ b/pype/plugins/celaction/_unused_publish/append_celaction_ftrack_asset_name.py @@ -1,19 +1,19 @@ -import pyblish.api - - -class AppendCelactionFtrackAssetName(pyblish.api.InstancePlugin): - """ Appending "ftrackAssetName" """ - - label = "Ftrack Asset Name" - order = pyblish.api.CollectorOrder + 0.1 - - def process(self, instance): - - # skipping if not launched from ftrack - if "ftrackData" not in instance.context.data: - return - - ftrack_data = instance.context.data["ftrackData"] - - asset_name = ftrack_data["Task"]["name"] - instance.data["ftrackAssetName"] = asset_name +import pyblish.api + + +class AppendCelactionFtrackAssetName(pyblish.api.InstancePlugin): + """ Appending "ftrackAssetName" """ + + label = "Ftrack Asset Name" + order = pyblish.api.CollectorOrder + 0.1 + + def process(self, instance): + + # skipping if not launched from ftrack + if "ftrackData" not in instance.context.data: + return + + ftrack_data = instance.context.data["ftrackData"] + + asset_name = ftrack_data["Task"]["name"] + instance.data["ftrackAssetName"] = asset_name diff --git a/pype/plugins/celaction/publish/append_celaction_ftrack_data.py b/pype/plugins/celaction/_unused_publish/append_celaction_ftrack_data.py similarity index 96% rename from pype/plugins/celaction/publish/append_celaction_ftrack_data.py rename to pype/plugins/celaction/_unused_publish/append_celaction_ftrack_data.py index fab3ae8b1a..552f4ffb1a 100644 --- a/pype/plugins/celaction/publish/append_celaction_ftrack_data.py +++ b/pype/plugins/celaction/_unused_publish/append_celaction_ftrack_data.py @@ -1,46 +1,46 @@ -import pyblish.api -from bait.ftrack.query_runner import QueryRunner - - -class AppendCelactionFtrackAudio(pyblish.api.ContextPlugin): - - label = "Ftrack Audio" - order = pyblish.api.ExtractorOrder - - def process(self, context): - - if context.data.get("audio", ''): - self.log.info('Audio data are already collected') - self.log.info('Audio: {}'.format(context.data.get("audio", ''))) - return - - runner = QueryRunner(context.data['ftrackSession']) - - audio_file = runner.get_audio_file_for_shot( - context.data['ftrackData']["Shot"]["id"]) - - if audio_file: - context.data["audio"] = { - 'filename': audio_file, - 'enabled': True - } - else: - self.log.warning("Couldn't find any audio file on Ftrack.") - - -class AppendCelactionFtrackData(pyblish.api.InstancePlugin): - """ Appending ftrack component and asset type data """ - - families = ["img.*", "mov.*"] - # offset to piggy back from default collectors - order = pyblish.api.CollectorOrder + 0.1 - - def process(self, instance): - - # ftrack data - if not instance.context.has_data("ftrackData"): - return - - instance.data["ftrackComponents"] = {} - asset_type = instance.data["family"].split(".")[0] - instance.data["ftrackAssetType"] = asset_type +import pyblish.api +from bait.ftrack.query_runner import QueryRunner + + +class AppendCelactionFtrackAudio(pyblish.api.ContextPlugin): + + label = "Ftrack Audio" + order = pyblish.api.ExtractorOrder + + def process(self, context): + + if context.data.get("audio", ''): + self.log.info('Audio data are already collected') + self.log.info('Audio: {}'.format(context.data.get("audio", ''))) + return + + runner = QueryRunner(context.data['ftrackSession']) + + audio_file = runner.get_audio_file_for_shot( + context.data['ftrackData']["Shot"]["id"]) + + if audio_file: + context.data["audio"] = { + 'filename': audio_file, + 'enabled': True + } + else: + self.log.warning("Couldn't find any audio file on Ftrack.") + + +class AppendCelactionFtrackData(pyblish.api.InstancePlugin): + """ Appending ftrack component and asset type data """ + + families = ["img.*", "mov.*"] + # offset to piggy back from default collectors + order = pyblish.api.CollectorOrder + 0.1 + + def process(self, instance): + + # ftrack data + if not instance.context.has_data("ftrackData"): + return + + instance.data["ftrackComponents"] = {} + asset_type = instance.data["family"].split(".")[0] + instance.data["ftrackAssetType"] = asset_type diff --git a/pype/plugins/celaction/publish/extract_celaction_deadline_old.py b/pype/plugins/celaction/_unused_publish/extract_celaction_deadline_old.py similarity index 100% rename from pype/plugins/celaction/publish/extract_celaction_deadline_old.py rename to pype/plugins/celaction/_unused_publish/extract_celaction_deadline_old.py diff --git a/pype/plugins/celaction/publish/submit_publish_job.py b/pype/plugins/celaction/_unused_publish/submit_publish_job.py similarity index 97% rename from pype/plugins/celaction/publish/submit_publish_job.py rename to pype/plugins/celaction/_unused_publish/submit_publish_job.py index 8d73dc245f..2767378b4a 100644 --- a/pype/plugins/celaction/publish/submit_publish_job.py +++ b/pype/plugins/celaction/_unused_publish/submit_publish_job.py @@ -1,283 +1,283 @@ -import os -import json -import pprint -import re -import requests -import pyblish.api - - -class SubmitDependentImageSequenceJobDeadline(pyblish.api.InstancePlugin): - """Submit image sequence publish jobs to Deadline. - - These jobs are dependent on a deadline job submission prior to this - plug-in. - - Renders are submitted to a Deadline Web Service as - supplied via the environment variable DEADLINE_REST_URL - - Options in instance.data: - - deadlineSubmission (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" - - This requires a "startFrame" and "endFrame" to be present in instance.data - or in context.data. - - """ - - label = "Submit image sequence jobs to Deadline" - order = pyblish.api.IntegratorOrder + 0.2 - - hosts = ["celaction"] - - families = [ - "render", - "deadline" - ] - - def process(self, instance): - - # DEADLINE_REST_URL = os.environ.get("DEADLINE_REST_URL", - # "http://localhost:8082") - # assert DEADLINE_REST_URL, "Requires DEADLINE_REST_URL" - - # Get a submission job - - job = instance.data.get("deadlineSubmissionJob") - if not job: - raise RuntimeError("Can't continue without valid deadline " - "submission prior to this plug-in.") - ################ - ft_data = instance.context.data["ftrackData"] - project = ft_data['Project']['name'] - project_code = ft_data['Project']['code'] - projects_path = os.path.dirname(ft_data['Project']['root']) - - data = instance.data.copy() - asset = instance.context.data["ftrackData"]['Shot']['name'] - subset = 'render' + \ - instance.context.data["ftrackData"]['Task']['name'].capitalize() - - state = data.get("publishJobState", "Suspended") - # job_name = "{batch} - {subset} [publish image sequence]".format( - # batch=job["Props"]["Name"], - # subset=subset - # ) - job_name = "{asset} [publish image sequence]".format( - asset=asset - ) - - # Get start/end frame from instance, if not available get from context - context = instance.context - - start = int(instance.data['start']) - end = int(instance.data['end']) - - try: - source = data['source'] - except KeyError: - source = context.data["currentFile"] - - # Write metadata for publish job - render_job = data.pop("deadlineSubmissionJob") - metadata = { - "asset": asset, - "regex": r"^.*\.png", - "subset": subset, - "startFrame": start, - "endFrame": end, - "fps": context.data.get("fps", None), - "families": ["render"], - "source": source, - "user": context.data["user"], - "version": context.data.get('version'), - "audio": context.data["audio"]['filename'], - # Optional metadata (for debugging) - "metadata": { - "instance": data, - "job": job, - "session": fake_avalon_session(project, projects_path) - } - } - - # Ensure output dir exists - output_dir = instance.data["outputDir"] - - if not os.path.isdir(output_dir): - os.makedirs(output_dir) - - for k, v in metadata.items(): - self.log.info(k) - self.log.info(v) - - metadata_filename = "{}_metadata.json".format(subset) - metadata_path = os.path.join(output_dir, metadata_filename) - with open(metadata_path, "w") as f: - json.dump(metadata, f, indent=4, sort_keys=True) - - # Generate the payload for Deadline submission - payload = { - "JobInfo": { - "Plugin": "Python", - "BatchName": job["Props"]["Batch"], - "Name": job_name, - "JobType": "Normal", - "Group": "celaction", - "JobDependency0": job["_id"], - "UserName": os.environ['USERNAME'], - "Comment": instance.context.data.get("comment", ""), - "InitialStatus": "Active" - }, - "PluginInfo": { - "Version": "3.6", - "ScriptFile": r"\\pype\Core\dev\pype-setup\repos\pype-config\pype\scripts\publish_filesequence.py", - "Arguments": '--path "{}"'.format(metadata_path), - "SingleFrameOnly": "True" - }, - - # Mandatory for Deadline, may be empty - "AuxFiles": [] - } - - # Transfer the environment from the original job to this dependent - # job so they use the same environment - environment = fake_env() - environment["AVALON_ASSET"] = asset - environment["AVALON_TASK"] = instance.context.data["ftrackData"]['Task']['name'] - environment["AVALON_PROJECT"] = project - environment["AVALON_PROJECTS"] = projects_path - environment["PYPE_STUDIO_PROJECTS_PUBLISH"] = ft_data['Project']['root'] - environment["PYPE_STUDIO_PROJECTS_RENDER"] = ft_data['Project']['root'] - environment["PYPE_STUDIO_PROJECTS_RESOURCES"] = ft_data['Project']['root'] - environment["PYPE_STUDIO_PROJECTS_WORK"] = ft_data['Project']['root'] - - payload["JobInfo"].update({ - "EnvironmentKeyValue%d" % index: "{key}={value}".format( - key=key, - value=environment[key] - ) for index, key in enumerate(environment) - }) - - # Avoid copied pools and remove secondary pool - payload["JobInfo"]["Pool"] = "animation_2d" - payload["JobInfo"].pop("SecondaryPool", None) - - self.log.info("Submitting..") - # self.log.info(json.dumps(payload, indent=4, sort_keys=True)) - - ################ - ###################### - fake_instance = instance.context.create_instance( - name=(str(instance) + "1")) - - for k, v in data.items(): - self.log.info(k) - fake_instance.data[k] = v - - # fake_instance.data['deadlineData'] = payload - # 'http://192.168.146.8:8082' - url = "{}/api/jobs".format('http://192.168.146.8:8082') - response = requests.post(url, json=payload) - if not response.ok: - raise Exception(response.text) - ###################### - ####################### - - -def fake_avalon_session(project=None, projects_path=None): - return { - "AVALON_APP": "premiere", - "AVALON_APP_VERSION": "2019", - "AVALON_ASSET": "editorial", - "AVALON_CONFIG": "pype", - "AVALON_CONTAINER_ID": "avalon.container", - "AVALON_DB": "Pype", - "AVALON_DEADLINE": "http://192.168.146.8:8082", - "AVALON_DEBUG": "1", - "AVALON_EARLY_ADOPTER": "1", - "AVALON_INSTANCE_ID": "avalon.instance", - "AVALON_LABEL": "Avalon", - "AVALON_LOCATION": "http://127.0.0.1", - "AVALON_MONGO": "mongodb://PypeAdmin:X34vkuwL4wbK9A7X@192.168.146.24:27072/Pype", - "AVALON_PASSWORD": "secret", - "AVALON_PROJECT": project or "LBB2_dev", - "AVALON_PROJECTS": projects_path or "L:/PYPE_test", - "AVALON_SILO": "editorial", - "AVALON_TASK": "conform", - "AVALON_TIMEOUT": "1000", - "AVALON_USERNAME": "avalon", - "AVALON_WORKDIR": "L:/PYPE_test/episodes/editorial/work/conform", - "schema": "avalon-core:session-1.0" - } - - -def fake_env(): - return { - "AVALON_CONFIG": "pype", - "AVALON_CONTAINER_ID": "avalon.container", - "AVALON_CORE": "\\\\pype\\Core\\dev\\pype-setup\\repos\\avalon-core", - "AVALON_DB": "Pype", - "AVALON_DB_DATA": "\\\\pype\\Core\\dev\\mongo_db_data", - "AVALON_DEADLINE": "http://192.168.146.8:8082", - "AVALON_DEBUG": "1", - "AVALON_EARLY_ADOPTER": "1", - "AVALON_ENV_NAME": "pype_env", - "AVALON_HIERARCHY": "", - "AVALON_INSTANCE_ID": "avalon.instance", - "AVALON_LABEL": "Avalon", - "AVALON_LAUNCHER": "\\\\pype\\Core\\dev\\pype-setup\\repos\\avalon-launcher", - "AVALON_LOCATION": "http://127.0.0.1", - "AVALON_MONGO": "mongodb://PypeAdmin:X34vkuwL4wbK9A7X@192.168.146.24:27072/Pype", - "AVALON_MONGO_PORT": "27072", - "AVALON_PASSWORD": "secret", - "AVALON_SCHEMA": "\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates\\schema", - "AVALON_SILO": "", - "AVALON_TIMEOUT": "1000", - "AVALON_USERNAME": "avalon", - "AVALON_WORKDIR": "default", - "DEADLINE_PATH": "C:\\Program Files\\Thinkbox\\Deadline10\\bin", - "DEADLINE_REST_URL": "http://192.168.146.8:8082", - "FTRACK_API_KEY": "NGI0ZGU3ZjMtNzNiZC00NGVlLWEwY2EtMzA1OWJlZGM0MjAyOjozZWZmMThjZi04MjkwLTQxMzQtODUwMC03NTZhMGJiZTM2MTA", - "FTRACK_API_USER": "license@clothcatanimation.com", - "FTRACK_SERVER": "https://clothcat2.ftrackapp.com", - "MONGO_DB_ENTRYDB": "Pype", - "MONGO_DB_PASS": "X34vkuwL4wbK9A7X", - "MONGO_DB_USER": "PypeAdmin", - "PATH": "\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\Scripts;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\Library\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\Library;;\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates\\bin;\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates\\bin\\windows;\\\\pype\\Core\\dev\\pype-setup\\app;\\\\pype\\core\\software\\ffmpeg\\bin;\\\\pype\\Core\\dev\\Applications\\djv\\bin", - "PYBLISHPLUGINPATH": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pype-config\\pype\\plugins\\ftrack\\publish;", - "PYBLISH_BASE": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-base", - "PYBLISH_HOSTS": "shell", - "PYBLISH_LITE": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-lite", - "PYBLISH_QML": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-qml", - "PYPE_APP_ROOT": "\\\\pype\\Core\\dev\\pype-setup\\app", - "PYPE_DEBUG": "3", - "PYPE_DEBUG_STDOUT": "0", - "PYPE_SETUP_ROOT": "\\\\pype\\Core\\dev\\pype-setup", - "PYPE_STUDIO_CODE": "CC", - "PYPE_STUDIO_CONFIG": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pype-config", - "PYPE_STUDIO_CORE": "\\\\pype\\Core\\dev", - "PYPE_STUDIO_CORE_MOUNT": "\\\\pype\\Core\\dev", - "PYPE_STUDIO_NAME": "Cloth Cat", - "PYPE_STUDIO_SOFTWARE": "\\\\pype\\Core\\dev\\Applications", - "PYPE_STUDIO_TEMPLATES": "\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates", - "PYPE_STUDIO_TOOLS": "\\\\pype\\Core\\dev\\production\\tools", - "PYTHONPATH": "\\\\pype\\Core\\dev\\pype-setup;\\\\pype\\Core\\dev\\pype-setup\\app\\vendor;\\\\pype\\Core\\dev\\pype-setup\\repos\\avalon-core;\\\\pype\\Core\\dev\\pype-setup\\repos\\avalon-launcher;\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-base;\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-qml;\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-lite;\\\\pype\\Core\\dev\\pype-setup\\repos\\ftrack-event-server;\\\\pype\\Core\\dev\\pype-setup\\repos\\pype-config;\\\\pype\\Core\\dev\\pype-setup\\app\\vendor;\\\\pype\\Core\\dev\\pype-setup\\repos\\pype-config\\pype\\vendor;\\\\pype\\Core\\dev\\pype-setup\\repos\\ftrack-event-server", - "PYTHONVERBOSE": "True", - "PYTHON_ENV": "C:\\Users\\Public\\pype_env", - "REMOTE_ENV_DIR": "\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env", - "REMOTE_ENV_ON": "0", - "SCHEMA": "avalon-core:session-1.0", - "STUDIO_SOFT": "\\\\evo2\\core\\Applications", - "TOOL_ENV": "\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates\\environments", - "USERNAME": "pype" - } +import os +import json +import pprint +import re +import requests +import pyblish.api + + +class SubmitDependentImageSequenceJobDeadline(pyblish.api.InstancePlugin): + """Submit image sequence publish jobs to Deadline. + + These jobs are dependent on a deadline job submission prior to this + plug-in. + + Renders are submitted to a Deadline Web Service as + supplied via the environment variable DEADLINE_REST_URL + + Options in instance.data: + - deadlineSubmission (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" + + This requires a "startFrame" and "endFrame" to be present in instance.data + or in context.data. + + """ + + label = "Submit image sequence jobs to Deadline" + order = pyblish.api.IntegratorOrder + 0.2 + + hosts = ["celaction"] + + families = [ + "render", + "deadline" + ] + + def process(self, instance): + + # DEADLINE_REST_URL = os.environ.get("DEADLINE_REST_URL", + # "http://localhost:8082") + # assert DEADLINE_REST_URL, "Requires DEADLINE_REST_URL" + + # Get a submission job + + job = instance.data.get("deadlineSubmissionJob") + if not job: + raise RuntimeError("Can't continue without valid deadline " + "submission prior to this plug-in.") + ################ + ft_data = instance.context.data["ftrackData"] + project = ft_data['Project']['name'] + project_code = ft_data['Project']['code'] + projects_path = os.path.dirname(ft_data['Project']['root']) + + data = instance.data.copy() + asset = instance.context.data["ftrackData"]['Shot']['name'] + subset = 'render' + \ + instance.context.data["ftrackData"]['Task']['name'].capitalize() + + state = data.get("publishJobState", "Suspended") + # job_name = "{batch} - {subset} [publish image sequence]".format( + # batch=job["Props"]["Name"], + # subset=subset + # ) + job_name = "{asset} [publish image sequence]".format( + asset=asset + ) + + # Get start/end frame from instance, if not available get from context + context = instance.context + + start = int(instance.data['start']) + end = int(instance.data['end']) + + try: + source = data['source'] + except KeyError: + source = context.data["currentFile"] + + # Write metadata for publish job + render_job = data.pop("deadlineSubmissionJob") + metadata = { + "asset": asset, + "regex": r"^.*\.png", + "subset": subset, + "startFrame": start, + "endFrame": end, + "fps": context.data.get("fps", None), + "families": ["render"], + "source": source, + "user": context.data["user"], + "version": context.data.get('version'), + "audio": context.data["audio"]['filename'], + # Optional metadata (for debugging) + "metadata": { + "instance": data, + "job": job, + "session": fake_avalon_session(project, projects_path) + } + } + + # Ensure output dir exists + output_dir = instance.data["outputDir"] + + if not os.path.isdir(output_dir): + os.makedirs(output_dir) + + for k, v in metadata.items(): + self.log.info(k) + self.log.info(v) + + metadata_filename = "{}_metadata.json".format(subset) + metadata_path = os.path.join(output_dir, metadata_filename) + with open(metadata_path, "w") as f: + json.dump(metadata, f, indent=4, sort_keys=True) + + # Generate the payload for Deadline submission + payload = { + "JobInfo": { + "Plugin": "Python", + "BatchName": job["Props"]["Batch"], + "Name": job_name, + "JobType": "Normal", + "Group": "celaction", + "JobDependency0": job["_id"], + "UserName": os.environ['USERNAME'], + "Comment": instance.context.data.get("comment", ""), + "InitialStatus": "Active" + }, + "PluginInfo": { + "Version": "3.6", + "ScriptFile": r"\\pype\Core\dev\pype-setup\repos\pype-config\pype\scripts\publish_filesequence.py", + "Arguments": '--path "{}"'.format(metadata_path), + "SingleFrameOnly": "True" + }, + + # Mandatory for Deadline, may be empty + "AuxFiles": [] + } + + # Transfer the environment from the original job to this dependent + # job so they use the same environment + environment = fake_env() + environment["AVALON_ASSET"] = asset + environment["AVALON_TASK"] = instance.context.data["ftrackData"]['Task']['name'] + environment["AVALON_PROJECT"] = project + environment["AVALON_PROJECTS"] = projects_path + environment["PYPE_STUDIO_PROJECTS_PUBLISH"] = ft_data['Project']['root'] + environment["PYPE_STUDIO_PROJECTS_RENDER"] = ft_data['Project']['root'] + environment["PYPE_STUDIO_PROJECTS_RESOURCES"] = ft_data['Project']['root'] + environment["PYPE_STUDIO_PROJECTS_WORK"] = ft_data['Project']['root'] + + payload["JobInfo"].update({ + "EnvironmentKeyValue%d" % index: "{key}={value}".format( + key=key, + value=environment[key] + ) for index, key in enumerate(environment) + }) + + # Avoid copied pools and remove secondary pool + payload["JobInfo"]["Pool"] = "animation_2d" + payload["JobInfo"].pop("SecondaryPool", None) + + self.log.info("Submitting..") + # self.log.info(json.dumps(payload, indent=4, sort_keys=True)) + + ################ + ###################### + fake_instance = instance.context.create_instance( + name=(str(instance) + "1")) + + for k, v in data.items(): + self.log.info(k) + fake_instance.data[k] = v + + # fake_instance.data['deadlineData'] = payload + # 'http://192.168.146.8:8082' + url = "{}/api/jobs".format('http://192.168.146.8:8082') + response = requests.post(url, json=payload) + if not response.ok: + raise Exception(response.text) + ###################### + ####################### + + +def fake_avalon_session(project=None, projects_path=None): + return { + "AVALON_APP": "premiere", + "AVALON_APP_VERSION": "2019", + "AVALON_ASSET": "editorial", + "AVALON_CONFIG": "pype", + "AVALON_CONTAINER_ID": "avalon.container", + "AVALON_DB": "Pype", + "AVALON_DEADLINE": "http://192.168.146.8:8082", + "AVALON_DEBUG": "1", + "AVALON_EARLY_ADOPTER": "1", + "AVALON_INSTANCE_ID": "avalon.instance", + "AVALON_LABEL": "Avalon", + "AVALON_LOCATION": "http://127.0.0.1", + "AVALON_MONGO": "mongodb://PypeAdmin:X34vkuwL4wbK9A7X@192.168.146.24:27072/Pype", + "AVALON_PASSWORD": "secret", + "AVALON_PROJECT": project or "LBB2_dev", + "AVALON_PROJECTS": projects_path or "L:/PYPE_test", + "AVALON_SILO": "editorial", + "AVALON_TASK": "conform", + "AVALON_TIMEOUT": "1000", + "AVALON_USERNAME": "avalon", + "AVALON_WORKDIR": "L:/PYPE_test/episodes/editorial/work/conform", + "schema": "avalon-core:session-1.0" + } + + +def fake_env(): + return { + "AVALON_CONFIG": "pype", + "AVALON_CONTAINER_ID": "avalon.container", + "AVALON_CORE": "\\\\pype\\Core\\dev\\pype-setup\\repos\\avalon-core", + "AVALON_DB": "Pype", + "AVALON_DB_DATA": "\\\\pype\\Core\\dev\\mongo_db_data", + "AVALON_DEADLINE": "http://192.168.146.8:8082", + "AVALON_DEBUG": "1", + "AVALON_EARLY_ADOPTER": "1", + "AVALON_ENV_NAME": "pype_env", + "AVALON_HIERARCHY": "", + "AVALON_INSTANCE_ID": "avalon.instance", + "AVALON_LABEL": "Avalon", + "AVALON_LAUNCHER": "\\\\pype\\Core\\dev\\pype-setup\\repos\\avalon-launcher", + "AVALON_LOCATION": "http://127.0.0.1", + "AVALON_MONGO": "mongodb://PypeAdmin:X34vkuwL4wbK9A7X@192.168.146.24:27072/Pype", + "AVALON_MONGO_PORT": "27072", + "AVALON_PASSWORD": "secret", + "AVALON_SCHEMA": "\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates\\schema", + "AVALON_SILO": "", + "AVALON_TIMEOUT": "1000", + "AVALON_USERNAME": "avalon", + "AVALON_WORKDIR": "default", + "DEADLINE_PATH": "C:\\Program Files\\Thinkbox\\Deadline10\\bin", + "DEADLINE_REST_URL": "http://192.168.146.8:8082", + "FTRACK_API_KEY": "NGI0ZGU3ZjMtNzNiZC00NGVlLWEwY2EtMzA1OWJlZGM0MjAyOjozZWZmMThjZi04MjkwLTQxMzQtODUwMC03NTZhMGJiZTM2MTA", + "FTRACK_API_USER": "license@clothcatanimation.com", + "FTRACK_SERVER": "https://clothcat2.ftrackapp.com", + "MONGO_DB_ENTRYDB": "Pype", + "MONGO_DB_PASS": "X34vkuwL4wbK9A7X", + "MONGO_DB_USER": "PypeAdmin", + "PATH": "\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\Scripts;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\Library\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\Library;;\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates\\bin;\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates\\bin\\windows;\\\\pype\\Core\\dev\\pype-setup\\app;\\\\pype\\core\\software\\ffmpeg\\bin;\\\\pype\\Core\\dev\\Applications\\djv\\bin", + "PYBLISHPLUGINPATH": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pype-config\\pype\\plugins\\ftrack\\publish;", + "PYBLISH_BASE": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-base", + "PYBLISH_HOSTS": "shell", + "PYBLISH_LITE": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-lite", + "PYBLISH_QML": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-qml", + "PYPE_APP_ROOT": "\\\\pype\\Core\\dev\\pype-setup\\app", + "PYPE_DEBUG": "3", + "PYPE_DEBUG_STDOUT": "0", + "PYPE_SETUP_ROOT": "\\\\pype\\Core\\dev\\pype-setup", + "PYPE_STUDIO_CODE": "CC", + "PYPE_STUDIO_CONFIG": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pype-config", + "PYPE_STUDIO_CORE": "\\\\pype\\Core\\dev", + "PYPE_STUDIO_CORE_MOUNT": "\\\\pype\\Core\\dev", + "PYPE_STUDIO_NAME": "Cloth Cat", + "PYPE_STUDIO_SOFTWARE": "\\\\pype\\Core\\dev\\Applications", + "PYPE_STUDIO_TEMPLATES": "\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates", + "PYPE_STUDIO_TOOLS": "\\\\pype\\Core\\dev\\production\\tools", + "PYTHONPATH": "\\\\pype\\Core\\dev\\pype-setup;\\\\pype\\Core\\dev\\pype-setup\\app\\vendor;\\\\pype\\Core\\dev\\pype-setup\\repos\\avalon-core;\\\\pype\\Core\\dev\\pype-setup\\repos\\avalon-launcher;\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-base;\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-qml;\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-lite;\\\\pype\\Core\\dev\\pype-setup\\repos\\ftrack-event-server;\\\\pype\\Core\\dev\\pype-setup\\repos\\pype-config;\\\\pype\\Core\\dev\\pype-setup\\app\\vendor;\\\\pype\\Core\\dev\\pype-setup\\repos\\pype-config\\pype\\vendor;\\\\pype\\Core\\dev\\pype-setup\\repos\\ftrack-event-server", + "PYTHONVERBOSE": "True", + "PYTHON_ENV": "C:\\Users\\Public\\pype_env", + "REMOTE_ENV_DIR": "\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env", + "REMOTE_ENV_ON": "0", + "SCHEMA": "avalon-core:session-1.0", + "STUDIO_SOFT": "\\\\evo2\\core\\Applications", + "TOOL_ENV": "\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates\\environments", + "USERNAME": "pype" + } diff --git a/pype/plugins/celaction/publish/validate_celaction_scene_path.py b/pype/plugins/celaction/_unused_publish/validate_celaction_scene_path.py similarity index 96% rename from pype/plugins/celaction/publish/validate_celaction_scene_path.py rename to pype/plugins/celaction/_unused_publish/validate_celaction_scene_path.py index a73819b9ca..17365178ac 100644 --- a/pype/plugins/celaction/publish/validate_celaction_scene_path.py +++ b/pype/plugins/celaction/_unused_publish/validate_celaction_scene_path.py @@ -1,62 +1,62 @@ -import shutil -import pyblish.api -import pyblish_standalone -import os -from bait.paths import get_env_work_file - - -class RepairCelactionScenePath(pyblish.api.Action): - label = "Repair" - icon = "wrench" - on = "failed" - - def process(self, context, plugin): - - # get version data - version = context.data('version') if context.has_data('version') else 1 - - task_id = context.data["ftrackData"]["Task"]["id"] - expected_path = get_env_work_file( - "celaction", task_id, "scn", version).replace('\\\\\\', '\\\\') - - src = context.data["currentFile"] - - if not os.path.exists(os.path.dirname(expected_path)): - os.makedirs(os.path.dirname(expected_path)) - - if os.path.exists(os.path.dirname(expected_path)): - self.log.info("existing to \"%s\"" % expected_path) - - if os.path.exists(expected_path) and ('v001' in expected_path): - os.remove(expected_path) - - shutil.copy2(src, expected_path) - - pyblish_standalone.kwargs['path'] = [expected_path] - context.data["currentFile"] = expected_path - - self.log.info("Saved to \"%s\"" % expected_path) - - -class ValidateCelactionScenePath(pyblish.api.InstancePlugin): - order = pyblish.api.ValidatorOrder - families = ['scene'] - label = 'Scene Path' - actions = [RepairCelactionScenePath] - - def process(self, instance): - - # getting current work file - current_scene_path = pyblish_standalone.kwargs['path'][0] - - version = instance.context.data( - 'version') if instance.context.has_data('version') else 1 - - task_id = instance.context.data["ftrackData"]["Task"]["id"] - expected_scene_path = get_env_work_file( - "celaction", task_id, "scn", version).replace('\\\\\\', '\\\\') - - msg = 'Scene path is not correct: Current: {}, Expected: {}'.format( - current_scene_path, expected_scene_path) - - assert expected_scene_path == current_scene_path, msg +import shutil +import pyblish.api +import pyblish_standalone +import os +from bait.paths import get_env_work_file + + +class RepairCelactionScenePath(pyblish.api.Action): + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + + # get version data + version = context.data('version') if context.has_data('version') else 1 + + task_id = context.data["ftrackData"]["Task"]["id"] + expected_path = get_env_work_file( + "celaction", task_id, "scn", version).replace('\\\\\\', '\\\\') + + src = context.data["currentFile"] + + if not os.path.exists(os.path.dirname(expected_path)): + os.makedirs(os.path.dirname(expected_path)) + + if os.path.exists(os.path.dirname(expected_path)): + self.log.info("existing to \"%s\"" % expected_path) + + if os.path.exists(expected_path) and ('v001' in expected_path): + os.remove(expected_path) + + shutil.copy2(src, expected_path) + + pyblish_standalone.kwargs['path'] = [expected_path] + context.data["currentFile"] = expected_path + + self.log.info("Saved to \"%s\"" % expected_path) + + +class ValidateCelactionScenePath(pyblish.api.InstancePlugin): + order = pyblish.api.ValidatorOrder + families = ['scene'] + label = 'Scene Path' + actions = [RepairCelactionScenePath] + + def process(self, instance): + + # getting current work file + current_scene_path = pyblish_standalone.kwargs['path'][0] + + version = instance.context.data( + 'version') if instance.context.has_data('version') else 1 + + task_id = instance.context.data["ftrackData"]["Task"]["id"] + expected_scene_path = get_env_work_file( + "celaction", task_id, "scn", version).replace('\\\\\\', '\\\\') + + msg = 'Scene path is not correct: Current: {}, Expected: {}'.format( + current_scene_path, expected_scene_path) + + assert expected_scene_path == current_scene_path, msg From 950644f630166e5cb2f566cf277a68a8f32fedf9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 30 Apr 2020 15:50:50 +0200 Subject: [PATCH 014/131] feat(celaction): wip publishing workfiles --- .../celaction/publish/collect_celaction_render.py | 4 +--- pype/plugins/celaction/publish/integrate_version_up.py | 9 ++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/pype/plugins/celaction/publish/collect_celaction_render.py b/pype/plugins/celaction/publish/collect_celaction_render.py index e6761633f9..2bf0309df8 100644 --- a/pype/plugins/celaction/publish/collect_celaction_render.py +++ b/pype/plugins/celaction/publish/collect_celaction_render.py @@ -18,12 +18,10 @@ class CollectCelactionRender(pyblish.api.ContextPlugin): instance = context.create_instance(name=component_name) instance.data["family"] = "render" instance.data["label"] = "{} - remote".format(component_name) - # instance.data["families"] = ["deadline", "remote", "img"] instance.data["families"] = ["render", "img"] - instance.data["managed_location"] = False # getting instance state - instance.data["publish"] = False + instance.data["publish"] = True data = context.data("kwargs")["data"] diff --git a/pype/plugins/celaction/publish/integrate_version_up.py b/pype/plugins/celaction/publish/integrate_version_up.py index 77c0d7a88b..7fb1efa8aa 100644 --- a/pype/plugins/celaction/publish/integrate_version_up.py +++ b/pype/plugins/celaction/publish/integrate_version_up.py @@ -1,5 +1,4 @@ import shutil -import os import re import pyblish.api @@ -30,12 +29,12 @@ def version_get(string, prefix, suffix=None): if string is None: raise ValueError("Empty version string - no match") - regex = "[/_.]" + prefix + "\d+" + regex = r"[/_.]{}\d+".format(prefix) matches = re.findall(regex, string, re.IGNORECASE) if not len(matches): - msg = "No \"_" + prefix + "#\" found in \"" + string + "\"" + msg = f"No `_{prefix}#` found in `{string}`" raise ValueError(msg) - return (matches[-1:][0][1], re.search("\d+", matches[-1:][0]).group()) + return (matches[-1:][0][1], re.search(r"\d+", matches[-1:][0]).group()) def version_set(string, prefix, oldintval, newintval): @@ -44,7 +43,7 @@ def version_set(string, prefix, oldintval, newintval): we use "v" for render version and "c" for camera track version. See the version.py and camera.py plugins for usage.""" - regex = "[/_.]" + prefix + "\d+" + regex = r"[/_.]{}\d+".format(prefix) matches = re.findall(regex, string, re.IGNORECASE) if not len(matches): return "" From 13dda8ef2412237f73cdfc9023eec371a7218fd8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 30 Apr 2020 15:33:09 +0100 Subject: [PATCH 015/131] celaction publishing wip --- .../collect_celaction_scene.py | 0 .../_unused_publish/collect_render_path.py | 60 +++++++++++++++++++ .../publish/collect_celaction_render.py | 24 +++++++- .../celaction/publish/collect_render_path.py | 59 +++++------------- 4 files changed, 95 insertions(+), 48 deletions(-) rename pype/plugins/celaction/{publish => _unused_publish}/collect_celaction_scene.py (100%) create mode 100644 pype/plugins/celaction/_unused_publish/collect_render_path.py diff --git a/pype/plugins/celaction/publish/collect_celaction_scene.py b/pype/plugins/celaction/_unused_publish/collect_celaction_scene.py similarity index 100% rename from pype/plugins/celaction/publish/collect_celaction_scene.py rename to pype/plugins/celaction/_unused_publish/collect_celaction_scene.py diff --git a/pype/plugins/celaction/_unused_publish/collect_render_path.py b/pype/plugins/celaction/_unused_publish/collect_render_path.py new file mode 100644 index 0000000000..72adf57e86 --- /dev/null +++ b/pype/plugins/celaction/_unused_publish/collect_render_path.py @@ -0,0 +1,60 @@ +""" +Requires: + context -> anatomy + context -> anatomyData + +Provides: + instance -> publishDir + instance -> resourcesDir +""" + +import os +import copy + +import pyblish.api +from avalon import api + + +class CollectRenderPath(pyblish.api.InstancePlugin): + """Generate file and directory path where rendered images will be""" + + label = "Collect Render Path" + order = pyblish.api.CollectorOrder + 0.495 + + def process(self, instance): + anatomy = instance.context.data["anatomy"] + + template_data = copy.deepcopy(instance.data["anatomyData"]) + + # This is for cases of Deprecated anatomy without `folder` + # TODO remove when all clients have solved this issue + template_data.update({ + "frame": "FRAME_TEMP", + "representation": "png" + }) + + anatomy_filled = anatomy.format(template_data) + + if "folder" in anatomy.templates["render"]: + render_folder = anatomy_filled["render"]["folder"] + render_file = anatomy_filled["render"]["file"] + else: + # solve deprecated situation when `folder` key is not underneath + # `publish` anatomy + project_name = api.Session["AVALON_PROJECT"] + self.log.warning(( + "Deprecation warning: Anatomy does not have set `folder`" + " key underneath `publish` (in global of for project `{}`)." + ).format(project_name)) + + file_path = anatomy_filled["render"]["path"] + # Directory + render_folder = os.path.dirname(file_path) + render_file = os.path.basename(file_path) + + render_folder = os.path.normpath(render_folder) + render_path = os.path.join(render_folder, render_file) + + instance.data["outputRenderPath"] = render_path + + self.log.debug("outputRenderPath: \"{}\"".format(render_path)) diff --git a/pype/plugins/celaction/publish/collect_celaction_render.py b/pype/plugins/celaction/publish/collect_celaction_render.py index 2bf0309df8..a8154e658f 100644 --- a/pype/plugins/celaction/publish/collect_celaction_render.py +++ b/pype/plugins/celaction/publish/collect_celaction_render.py @@ -6,9 +6,12 @@ import pyblish.api class CollectCelactionRender(pyblish.api.ContextPlugin): """ Adds the celaction render instances """ + label = "Collect Celaction Render Instance" order = pyblish.api.CollectorOrder + 0.1 def process(self, context): + project_entity = context.data["projectEntity"] + asset_entity = context.data["assetEntity"] # scene render scene_file = os.path.basename(context.data["currentFile"]) @@ -23,7 +26,22 @@ class CollectCelactionRender(pyblish.api.ContextPlugin): # getting instance state instance.data["publish"] = True - data = context.data("kwargs")["data"] + # add assetEntity data into instance + instance.data.update({ + "subset": "renderAnimationMain", + "asset": asset_entity["name"], + "frameStart": asset_entity["data"]["frameStart"], + "frameEnd": asset_entity["data"]["frameEnd"], + "handleStart": asset_entity["data"]["handleStart"], + "handleEnd": asset_entity["data"]["handleEnd"], + "fps": asset_entity["data"]["fps"], + "resolutionWidth": asset_entity["data"]["resolutionWidth"], + "resolutionHeight": asset_entity["data"]["resolutionHeight"], + "pixelAspect": 1, + "step": 1 + }) - for item in data: - instance.set_data(item, value=data[item]) + data = context.data.get("kwargs", {}).get("data", {}) + + if data: + instance.data.update(data) diff --git a/pype/plugins/celaction/publish/collect_render_path.py b/pype/plugins/celaction/publish/collect_render_path.py index 72adf57e86..a0d82fe4a5 100644 --- a/pype/plugins/celaction/publish/collect_render_path.py +++ b/pype/plugins/celaction/publish/collect_render_path.py @@ -1,18 +1,6 @@ -""" -Requires: - context -> anatomy - context -> anatomyData - -Provides: - instance -> publishDir - instance -> resourcesDir -""" - import os -import copy - import pyblish.api -from avalon import api + class CollectRenderPath(pyblish.api.InstancePlugin): @@ -22,39 +10,20 @@ class CollectRenderPath(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.495 def process(self, instance): - anatomy = instance.context.data["anatomy"] + current_file = instance.context.data["currentFile"] + work_dir = os.path.dirname(current_file) + work_file = os.path.basename(current_file) - template_data = copy.deepcopy(instance.data["anatomyData"]) + render_dir = os.path.join( + work_dir, "render", "celaction" + ) + render_path = os.path.join( + render_dir, ".".join([instance.data["subset"], "%05d", "png"]) + ) - # This is for cases of Deprecated anatomy without `folder` - # TODO remove when all clients have solved this issue - template_data.update({ - "frame": "FRAME_TEMP", - "representation": "png" - }) + # create dir if it doesnt exists + os.makedirs(render_dir, exist_ok=True) - anatomy_filled = anatomy.format(template_data) + instance.data["path"] = render_path - if "folder" in anatomy.templates["render"]: - render_folder = anatomy_filled["render"]["folder"] - render_file = anatomy_filled["render"]["file"] - else: - # solve deprecated situation when `folder` key is not underneath - # `publish` anatomy - project_name = api.Session["AVALON_PROJECT"] - self.log.warning(( - "Deprecation warning: Anatomy does not have set `folder`" - " key underneath `publish` (in global of for project `{}`)." - ).format(project_name)) - - file_path = anatomy_filled["render"]["path"] - # Directory - render_folder = os.path.dirname(file_path) - render_file = os.path.basename(file_path) - - render_folder = os.path.normpath(render_folder) - render_path = os.path.join(render_folder, render_file) - - instance.data["outputRenderPath"] = render_path - - self.log.debug("outputRenderPath: \"{}\"".format(render_path)) + self.log.info(f"Render output path set to: `{render_path}`") From d30df3a75a38221e7df8191333fbeedd74854c4f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 30 Apr 2020 16:34:55 +0100 Subject: [PATCH 016/131] feat(celaction): publishing wip --- pype/celaction/cli.py | 2 + .../collect_data.py | 0 ...rgs.py => collect_celaction_cli_kwargs.py} | 13 +-- .../publish/collect_celaction_instances.py | 85 +++++++++++++++++++ .../publish/collect_celaction_render.py | 47 ---------- .../publish/submit_celaction_deadline.py | 4 +- .../global/publish/submit_publish_job.py | 2 +- 7 files changed, 98 insertions(+), 55 deletions(-) rename pype/plugins/celaction/{publish => _unused_publish}/collect_data.py (100%) rename pype/plugins/celaction/publish/{collect_kwargs.py => collect_celaction_cli_kwargs.py} (50%) create mode 100644 pype/plugins/celaction/publish/collect_celaction_instances.py delete mode 100644 pype/plugins/celaction/publish/collect_celaction_render.py diff --git a/pype/celaction/cli.py b/pype/celaction/cli.py index f6d518a5a9..ade15effdf 100644 --- a/pype/celaction/cli.py +++ b/pype/celaction/cli.py @@ -108,6 +108,8 @@ def main(): log.info(f"Registering path: {path}") pyblish.api.register_plugin_path(path) + pyblish.api.register_host(publish_host) + # Register project specific plugins project_name = os.environ["AVALON_PROJECT"] project_plugins_paths = os.getenv("PYPE_PROJECT_PLUGINS", "") diff --git a/pype/plugins/celaction/publish/collect_data.py b/pype/plugins/celaction/_unused_publish/collect_data.py similarity index 100% rename from pype/plugins/celaction/publish/collect_data.py rename to pype/plugins/celaction/_unused_publish/collect_data.py diff --git a/pype/plugins/celaction/publish/collect_kwargs.py b/pype/plugins/celaction/publish/collect_celaction_cli_kwargs.py similarity index 50% rename from pype/plugins/celaction/publish/collect_kwargs.py rename to pype/plugins/celaction/publish/collect_celaction_cli_kwargs.py index b2e6d97b8b..d60eaba4c6 100644 --- a/pype/plugins/celaction/publish/collect_kwargs.py +++ b/pype/plugins/celaction/publish/collect_celaction_cli_kwargs.py @@ -2,16 +2,19 @@ import pyblish.api import pype.celaction -class CollectKwargs(pyblish.api.Collector): +class CollectCelactionCliKwargs(pyblish.api.Collector): """ Collects all keyword arguments passed from the terminal """ - + + label = "Collect Celaction Cli Kwargs" order = pyblish.api.Collector.order - 0.1 def process(self, context): kwargs = pype.celaction.kwargs.copy() - self.log.info("Converting nested lists to dict: %s" % kwargs) - kwargs["data"] = dict(kwargs.get("data") or []) - self.log.info("Storing kwargs: %s" % kwargs) context.set_data("kwargs", kwargs) + + # get kwargs onto context data as keys with values + for k, v in kwargs.items(): + self.log.info(f"Setting `{k}` to instance.data with value: `{v}`") + context.data[k] = v diff --git a/pype/plugins/celaction/publish/collect_celaction_instances.py b/pype/plugins/celaction/publish/collect_celaction_instances.py new file mode 100644 index 0000000000..84e4ded303 --- /dev/null +++ b/pype/plugins/celaction/publish/collect_celaction_instances.py @@ -0,0 +1,85 @@ +import os +from avalon import api +import pyblish.api + + +class CollectCelactionInstances(pyblish.api.ContextPlugin): + """ Adds the celaction render instances """ + + label = "Collect Celaction Instances" + order = pyblish.api.CollectorOrder + 0.1 + + def process(self, context): + task = api.Session["AVALON_TASK"] + current_file = context.data["currentFile"] + staging_dir = os.path.dirname(current_file) + scene_file = os.path.basename(current_file) + + asset_entity = context.data["assetEntity"] + + shared_instance_data = { + "asset": asset_entity["name"], + "frameStart": asset_entity["data"]["frameStart"], + "frameEnd": asset_entity["data"]["frameEnd"], + "handleStart": asset_entity["data"]["handleStart"], + "handleEnd": asset_entity["data"]["handleEnd"], + "fps": asset_entity["data"]["fps"], + "resolutionWidth": asset_entity["data"]["resolutionWidth"], + "resolutionHeight": asset_entity["data"]["resolutionHeight"], + "pixelAspect": 1, + "step": 1 + } + + celaction_kwargs = context.data.get("kwargs", {}) + + if celaction_kwargs: + shared_instance_data.update(celaction_kwargs) + + ##################################################3 + # workfile instance + family = "workfile" + subset = family + task.capitalize() + # Create instance + instance = context.create_instance(subset) + + # creating instance data + instance.data.update({ + "subset": subset, + "label": scene_file, + "family": family, + "families": [], + "representations": list() + }) + + # adding basic script data + instance.data.update(shared_instance_data) + + # creating representation + representation = { + 'name': 'scn', + 'ext': 'scn', + 'files': scene_file, + "stagingDir": staging_dir, + } + + instance.data["representations"].append(representation) + + self.log.info('Publishing Celaction workfile') + context.data["instances"].append(instance) + + ####################################################3 + # render instance + subset = f"render{task}Main" + instance = context.create_instance(name=subset) + # getting instance state + instance.data["publish"] = True + + # add assetEntity data into instance + instance.data.update({ + "label": "{} - farm".format(subset), + "family": "render.farm", + "families": [], + "subset": + }) + + self.log.debug(f"Instance data: `{instance.data}`") diff --git a/pype/plugins/celaction/publish/collect_celaction_render.py b/pype/plugins/celaction/publish/collect_celaction_render.py deleted file mode 100644 index a8154e658f..0000000000 --- a/pype/plugins/celaction/publish/collect_celaction_render.py +++ /dev/null @@ -1,47 +0,0 @@ -import os - -import pyblish.api - - -class CollectCelactionRender(pyblish.api.ContextPlugin): - """ Adds the celaction render instances """ - - label = "Collect Celaction Render Instance" - order = pyblish.api.CollectorOrder + 0.1 - - def process(self, context): - project_entity = context.data["projectEntity"] - asset_entity = context.data["assetEntity"] - - # scene render - scene_file = os.path.basename(context.data["currentFile"]) - scene_name, _ = os.path.splitext(scene_file) - component_name = scene_name.split(".")[0] - - instance = context.create_instance(name=component_name) - instance.data["family"] = "render" - instance.data["label"] = "{} - remote".format(component_name) - instance.data["families"] = ["render", "img"] - - # getting instance state - instance.data["publish"] = True - - # add assetEntity data into instance - instance.data.update({ - "subset": "renderAnimationMain", - "asset": asset_entity["name"], - "frameStart": asset_entity["data"]["frameStart"], - "frameEnd": asset_entity["data"]["frameEnd"], - "handleStart": asset_entity["data"]["handleStart"], - "handleEnd": asset_entity["data"]["handleEnd"], - "fps": asset_entity["data"]["fps"], - "resolutionWidth": asset_entity["data"]["resolutionWidth"], - "resolutionHeight": asset_entity["data"]["resolutionHeight"], - "pixelAspect": 1, - "step": 1 - }) - - data = context.data.get("kwargs", {}).get("data", {}) - - if data: - instance.data.update(data) diff --git a/pype/plugins/celaction/publish/submit_celaction_deadline.py b/pype/plugins/celaction/publish/submit_celaction_deadline.py index 3ea9b929e3..d2a62c2b0f 100644 --- a/pype/plugins/celaction/publish/submit_celaction_deadline.py +++ b/pype/plugins/celaction/publish/submit_celaction_deadline.py @@ -19,7 +19,7 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): label = "Submit CelAction to Deadline" order = pyblish.api.IntegratorOrder + 0.1 hosts = ["celaction"] - families = ["render"] + families = ["render.farm"] deadline_department = "" deadline_priority = 50 @@ -86,7 +86,7 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): pass # define chunk and priority - chunk_size = instance.data.get("deadlineChunkSize") + chunk_size = instance.context.data.get("chunk") if chunk_size == 0: chunk_size = self.deadline_chunk_size diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 843760f9ec..2dec6e090b 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -139,7 +139,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder + 0.2 icon = "tractor" - hosts = ["fusion", "maya", "nuke"] + hosts = ["fusion", "maya", "nuke", "celaction"] families = ["render.farm", "prerener", "renderlayer", "imagesequence"] From cb57b839e3f47bc3cd3358fc471bb4223c9cdcae Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 30 Apr 2020 17:37:35 +0200 Subject: [PATCH 017/131] feat(celaction): wip publishing --- .../celaction/publish/collect_celaction_instances.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/plugins/celaction/publish/collect_celaction_instances.py b/pype/plugins/celaction/publish/collect_celaction_instances.py index 84e4ded303..bf609ed90d 100644 --- a/pype/plugins/celaction/publish/collect_celaction_instances.py +++ b/pype/plugins/celaction/publish/collect_celaction_instances.py @@ -35,7 +35,7 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin): if celaction_kwargs: shared_instance_data.update(celaction_kwargs) - ##################################################3 + # ___________________________________________ # workfile instance family = "workfile" subset = family + task.capitalize() @@ -67,7 +67,7 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin): self.log.info('Publishing Celaction workfile') context.data["instances"].append(instance) - ####################################################3 + # ___________________________________________ # render instance subset = f"render{task}Main" instance = context.create_instance(name=subset) @@ -79,7 +79,7 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin): "label": "{} - farm".format(subset), "family": "render.farm", "families": [], - "subset": + "subset": subset }) self.log.debug(f"Instance data: `{instance.data}`") From 25f5ff72e9db920a86ec5c09d7aac8b73fe10411 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 1 May 2020 15:46:52 +0100 Subject: [PATCH 018/131] feat(celaction): publishing wip --- pype/celaction/cli.py | 4 +- pype/hooks/celaction/prelaunch.py | 2 +- .../publish/collect_celaction_cli_kwargs.py | 7 +- .../publish/collect_celaction_instances.py | 23 +++++-- .../celaction/publish/collect_render_path.py | 5 +- .../publish/submit_celaction_deadline.py | 65 ++++++++++++------- 6 files changed, 70 insertions(+), 36 deletions(-) diff --git a/pype/celaction/cli.py b/pype/celaction/cli.py index ade15effdf..daa971d8d5 100644 --- a/pype/celaction/cli.py +++ b/pype/celaction/cli.py @@ -52,8 +52,8 @@ def cli(): parser.add_argument("--resolutionHeight", help=("Height of resolution")) - parser.add_argument("--programDir", - help=("Directory with celaction program installation")) + # parser.add_argument("--programDir", + # help=("Directory with celaction program installation")) pype.celaction.kwargs = parser.parse_args(sys.argv[1:]).__dict__ diff --git a/pype/hooks/celaction/prelaunch.py b/pype/hooks/celaction/prelaunch.py index 9af3cdd740..2cd4b01381 100644 --- a/pype/hooks/celaction/prelaunch.py +++ b/pype/hooks/celaction/prelaunch.py @@ -88,7 +88,7 @@ class CelactionPrelaunchHook(PypeHook): "--frameEnd *END*", "--resolutionWidth *X*", "--resolutionHeight *Y*", - "--programDir \"'*PROGPATH*'\"" + # "--programDir \"'*PROGPATH*'\"" ] winreg.SetValueEx(hKey, "SubmitParametersTitle", 0, winreg.REG_SZ, " ".join(parameters)) diff --git a/pype/plugins/celaction/publish/collect_celaction_cli_kwargs.py b/pype/plugins/celaction/publish/collect_celaction_cli_kwargs.py index d60eaba4c6..5042a7b700 100644 --- a/pype/plugins/celaction/publish/collect_celaction_cli_kwargs.py +++ b/pype/plugins/celaction/publish/collect_celaction_cli_kwargs.py @@ -4,7 +4,7 @@ import pype.celaction class CollectCelactionCliKwargs(pyblish.api.Collector): """ Collects all keyword arguments passed from the terminal """ - + label = "Collect Celaction Cli Kwargs" order = pyblish.api.Collector.order - 0.1 @@ -17,4 +17,7 @@ class CollectCelactionCliKwargs(pyblish.api.Collector): # get kwargs onto context data as keys with values for k, v in kwargs.items(): self.log.info(f"Setting `{k}` to instance.data with value: `{v}`") - context.data[k] = v + if k in ["frameStart", "frameEnd"]: + context.data[k] = kwargs[k] = int(v) + else: + context.data[k] = v diff --git a/pype/plugins/celaction/publish/collect_celaction_instances.py b/pype/plugins/celaction/publish/collect_celaction_instances.py index bf609ed90d..583cb7514b 100644 --- a/pype/plugins/celaction/publish/collect_celaction_instances.py +++ b/pype/plugins/celaction/publish/collect_celaction_instances.py @@ -1,7 +1,7 @@ import os from avalon import api import pyblish.api - +from pype import api as pype class CollectCelactionInstances(pyblish.api.ContextPlugin): """ Adds the celaction render instances """ @@ -14,7 +14,7 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin): current_file = context.data["currentFile"] staging_dir = os.path.dirname(current_file) scene_file = os.path.basename(current_file) - + version = context.data["version"] asset_entity = context.data["assetEntity"] shared_instance_data = { @@ -27,7 +27,8 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin): "resolutionWidth": asset_entity["data"]["resolutionWidth"], "resolutionHeight": asset_entity["data"]["resolutionHeight"], "pixelAspect": 1, - "step": 1 + "step": 1, + "version": version } celaction_kwargs = context.data.get("kwargs", {}) @@ -47,7 +48,7 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin): "subset": subset, "label": scene_file, "family": family, - "families": [], + "families": [family], "representations": list() }) @@ -65,10 +66,10 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin): instance.data["representations"].append(representation) self.log.info('Publishing Celaction workfile') - context.data["instances"].append(instance) # ___________________________________________ # render instance + family = "render.farm" subset = f"render{task}Main" instance = context.create_instance(name=subset) # getting instance state @@ -77,9 +78,17 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin): # add assetEntity data into instance instance.data.update({ "label": "{} - farm".format(subset), - "family": "render.farm", - "families": [], + "family": family, + "families": [family], "subset": subset }) + # adding basic script data + instance.data.update(shared_instance_data) + + self.log.info('Publishing Celaction render instance') self.log.debug(f"Instance data: `{instance.data}`") + + + for i in context: + self.log.debug(f"{i.data['families']}") diff --git a/pype/plugins/celaction/publish/collect_render_path.py b/pype/plugins/celaction/publish/collect_render_path.py index a0d82fe4a5..32a4bf7c32 100644 --- a/pype/plugins/celaction/publish/collect_render_path.py +++ b/pype/plugins/celaction/publish/collect_render_path.py @@ -10,15 +10,16 @@ class CollectRenderPath(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.495 def process(self, instance): + anatomy = instance.context.data["anatomy"] current_file = instance.context.data["currentFile"] work_dir = os.path.dirname(current_file) work_file = os.path.basename(current_file) - + padding = anatomy.templates.get("frame_padding", 4) render_dir = os.path.join( work_dir, "render", "celaction" ) render_path = os.path.join( - render_dir, ".".join([instance.data["subset"], "%05d", "png"]) + render_dir, ".".join([instance.data["subset"], f"%0{padding}d", "png"]) ) # create dir if it doesnt exists diff --git a/pype/plugins/celaction/publish/submit_celaction_deadline.py b/pype/plugins/celaction/publish/submit_celaction_deadline.py index d2a62c2b0f..9e0739419b 100644 --- a/pype/plugins/celaction/publish/submit_celaction_deadline.py +++ b/pype/plugins/celaction/publish/submit_celaction_deadline.py @@ -33,17 +33,15 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): context = instance.context - DEADLINE_REST_URL = os.environ.get("DEADLINE_REST_URL", - "http://localhost:8082") + DEADLINE_REST_URL = os.environ.get("DEADLINE_REST_URL") assert DEADLINE_REST_URL, "Requires DEADLINE_REST_URL" self.deadline_url = "{}/api/jobs".format(DEADLINE_REST_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()) - self._frame_start = int(instance.data["frameStartHandle"]) - self._frame_end = int(instance.data["frameEndHandle"]) + self._frame_start = int(instance.data["frameStart"]) + self._frame_end = int(instance.data["frameEnd"]) # get output path render_path = instance.data['path'] @@ -60,9 +58,10 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): render_path).replace("\\", "/") instance.data["publishJobState"] = "Suspended" + instance.context.data['ftrackStatus'] = "Render" # adding 2d render specific family for version identification in Loader - instance.data["families"] = families.insert(0, "render2d") + instance.data["families"] = ["render2d"] def payload_submit(self, instance, @@ -70,6 +69,8 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): render_path, responce_data=None ): + resolution_width = instance.data["resolutionWidth"] + resolution_height = instance.data["resolutionHeight"] render_dir = os.path.normpath(os.path.dirname(render_path)) script_name = os.path.basename(script_path) jobname = "%s - %s" % (script_name, instance.name) @@ -90,14 +91,35 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): if chunk_size == 0: 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", + "-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": { - # Top-level group name - "BatchName": script_name, - # 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, @@ -109,11 +131,7 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): "Pool": self.deadline_pool, "SecondaryPool": self.deadline_pool_secondary, - "Plugin": "CelAction", - "Frames": "{start}-{end}".format( - start=self._frame_start, - end=self._frame_end - ), + "Frames": f"{self._frame_start}-{self._frame_end}", "Comment": self._comment, # Optional, enable double-click to preview rendered @@ -125,11 +143,12 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): # Input "SceneFile": script_path, - # Output directory and filename + # Output directory "OutputFilePath": render_dir.replace("\\", "/"), - # Mandatory for Deadline - "Version": self._ver.group(), + # Plugin attributes + "StartupDirectory": "", + "Arguments": " ".join(args), # Resolve relative references "ProjectPath": script_path, @@ -176,14 +195,14 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): elif os.pathsep not in to_process: try: path = environment[key] - path.decode('UTF-8', 'strict') + path.encode().decode('UTF-8', 'strict') clean_path = os.path.normpath(path) except UnicodeDecodeError: print('path contains non UTF characters') else: for path in environment[key].split(os.pathsep): try: - path.decode('UTF-8', 'strict') + path.encode().decode('UTF-8', 'strict') clean_path += os.path.normpath(path) + os.pathsep except UnicodeDecodeError: print('path contains non UTF characters') @@ -253,9 +272,11 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): """ 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]) + 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]]) if "#" in path: self.log.debug("_ path: `{}`".format(path)) return path From 6d816a87b8ee08fb00c2438c1f133082aecdf2fc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 1 May 2020 17:02:45 +0200 Subject: [PATCH 019/131] feat(celaction): improve message --- pype/hooks/celaction/prelaunch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hooks/celaction/prelaunch.py b/pype/hooks/celaction/prelaunch.py index 2cd4b01381..497d40ace0 100644 --- a/pype/hooks/celaction/prelaunch.py +++ b/pype/hooks/celaction/prelaunch.py @@ -58,7 +58,7 @@ class CelactionPrelaunchHook(PypeHook): project_file = os.path.join(workdir, workfile) env["PYPE_CELACTION_PROJECT_FILE"] = project_file - self.log.info(f"Workfile is: `{project_file}`") + self.log.info(f"Workfile to open: `{project_file}`") ########################## # setting output parameters From af055612ca74f29aab419367f5c2ae7690c45958 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 May 2020 15:01:20 +0100 Subject: [PATCH 020/131] submiting from presets and env filtering --- .../publish/submit_celaction_deadline.py | 52 ++----------------- .../global/publish/submit_publish_job.py | 26 +++++++--- 2 files changed, 24 insertions(+), 54 deletions(-) diff --git a/pype/plugins/celaction/publish/submit_celaction_deadline.py b/pype/plugins/celaction/publish/submit_celaction_deadline.py index 9e0739419b..1d7694b1f0 100644 --- a/pype/plugins/celaction/publish/submit_celaction_deadline.py +++ b/pype/plugins/celaction/publish/submit_celaction_deadline.py @@ -169,55 +169,13 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): # Include critical environment variables with submission keys = [ - "PYTHONPATH", - "PATH", - "AVALON_SCHEMA", - "PYBLISHPLUGINPATH", - "TOOL_ENV" + "FTRACK_API_USER", + "FTRACK_API_KEY", + "FTRACK_SERVER", + "AVALON_PROJECT" ] environment = dict({key: os.environ[key] for key in keys - if key in os.environ}, **api.Session) - - for path in os.environ: - if path.lower().startswith('pype_'): - environment[path] = os.environ[path] - - environment["PATH"] = os.environ["PATH"] - clean_environment = {} - for key in environment: - clean_path = "" - self.log.debug("key: {}".format(key)) - to_process = environment[key] - if key == "PYPE_STUDIO_CORE_MOUNT": - clean_path = environment[key] - elif "://" in environment[key]: - clean_path = environment[key] - elif os.pathsep not in to_process: - try: - path = environment[key] - path.encode().decode('UTF-8', 'strict') - clean_path = os.path.normpath(path) - except UnicodeDecodeError: - print('path contains non UTF characters') - else: - for path in environment[key].split(os.pathsep): - try: - path.encode().decode('UTF-8', 'strict') - clean_path += os.path.normpath(path) + os.pathsep - except UnicodeDecodeError: - print('path contains non UTF characters') - - if key == "PYTHONPATH": - clean_path = clean_path.replace('python2', 'python3') - - clean_path = clean_path.replace( - os.path.normpath( - environment['PYPE_STUDIO_CORE_MOUNT']), # noqa - os.path.normpath( - environment['PYPE_STUDIO_CORE_PATH'])) # noqa - clean_environment[key] = clean_path - - environment = clean_environment + if key in os.environ}) payload["JobInfo"].update({ "EnvironmentKeyValue%d" % index: "{key}={value}".format( diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 2dec6e090b..3f5f9bc1e5 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -146,20 +146,23 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): aov_filter = {"maya": ["beauty"]} enviro_filter = [ - "PATH", - "PYTHONPATH", + # "PYTHONPATH", "FTRACK_API_USER", "FTRACK_API_KEY", "FTRACK_SERVER", - "PYPE_ROOT", + "PYPE_LOG_NO_COLORS", "PYPE_METADATA_FILE", - "PYPE_STUDIO_PROJECTS_PATH", - "PYPE_STUDIO_PROJECTS_MOUNT", - "AVALON_PROJECT" + "AVALON_PROJECT", + "PYPE_PYTHON_EXE", + "PYTHONPATH" ] - # pool used to do the publishing job + # custom deadline atributes + deadline_department = "" deadline_pool = "" + deadline_pool_secondary = "" + deadline_group = "" + deadline_chunk_size = 1 # regex for finding frame number in string R_FRAME_NUMBER = re.compile(r'.+\.(?P[0-9]+)\..+') @@ -206,8 +209,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "JobDependency0": job["_id"], "UserName": job["Props"]["User"], "Comment": instance.context.data.get("comment", ""), + + "Department": self.deadline_department, + "ChunkSize": self.deadline_chunk_size, "Priority": job["Props"]["Pri"], + + "Group": self.deadline_group, "Pool": self.deadline_pool, + "SecondaryPool": self.deadline_pool_secondary, + "OutputDirectory0": output_dir }, "PluginInfo": { @@ -223,6 +233,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # Transfer the environment from the original job to this dependent # job so they use the same environment environment = job["Props"].get("Env", {}) + environment["PYPE_PYTHON_EXE"] = "//pype/Core/software/python36/python.exe" + environment["PYPE_LOG_NO_COLORS"] = "1" environment["PYPE_METADATA_FILE"] = metadata_path environment["AVALON_PROJECT"] = io.Session["AVALON_PROJECT"] i = 0 From 7369e41e67c46751949bdf923559a005cd671092 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 May 2020 18:18:04 +0100 Subject: [PATCH 021/131] feat(global): adding submission with audio to metadata.json --- pype/plugins/global/publish/submit_publish_job.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 3f5f9bc1e5..aa92a55c1a 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -146,15 +146,13 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): aov_filter = {"maya": ["beauty"]} enviro_filter = [ - # "PYTHONPATH", "FTRACK_API_USER", "FTRACK_API_KEY", "FTRACK_SERVER", "PYPE_LOG_NO_COLORS", "PYPE_METADATA_FILE", "AVALON_PROJECT", - "PYPE_PYTHON_EXE", - "PYTHONPATH" + "PYPE_PYTHON_EXE" ] # custom deadline atributes @@ -749,6 +747,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "instances": instances } + # add audio to metadata file if available + audio_file = context.data.get("audioFile") + if os.path.isfile(audio_file): + publish_job.update({ "audio": audio_file}) + # pass Ftrack credentials in case of Muster if submission_type == "muster": ftrack = { From 0a60e5fd5d769907be82eb355b851b7e554aa02a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 May 2020 18:18:45 +0100 Subject: [PATCH 022/131] feat(celaction): collecting audio representation path --- .../celaction/publish/collect_audio.py | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/pype/plugins/celaction/publish/collect_audio.py b/pype/plugins/celaction/publish/collect_audio.py index bf4e1f47f3..44b40f046c 100644 --- a/pype/plugins/celaction/publish/collect_audio.py +++ b/pype/plugins/celaction/publish/collect_audio.py @@ -1,38 +1,41 @@ import pyblish.api -from bait.paths import get_output_path import os +import pype.api as pype +from avalon import io +from pprint import pformat class AppendCelactionAudio(pyblish.api.ContextPlugin): - label = "Pype Audio" + label = "Colect Audio for publishing" order = pyblish.api.CollectorOrder + 0.1 def process(self, context): self.log.info('Collecting Audio Data') - version = context.data('version') if context.has_data('version') else 1 + asset_entity = context.data["assetEntity"] + asset_id = asset_entity["_id"] - task_id = context.data["ftrackData"]["Task"]["id"] + # get all available representations + subsets = pype.get_subsets(asset_entity["name"], + representations=["audio"] + ) + self.log.info(f"subsets is: {pformat(subsets)}") - component_name = context.data["ftrackData"]['Shot']['name'] - version = context.data["version"] + if not subsets.get("audioMain"): + raise AttributeError("`audioMain` subset does not exist") - publish_path = get_output_path( - task_id, component_name, version, "mov").split('/')[0:-4] + reprs = subsets.get("audioMain", {}).get("representations", []) + self.log.info(f"reprs is: {pformat(reprs)}") - self.log.info('publish_path: {}'.format(publish_path)) + repr = next((r for r in reprs), None) + if not repr: + raise "Missing `audioMain` representation" + self.log.info(f"represetation is: {repr}") - audio_file = '/'.join(publish_path + [ - 'audio', - 'audioMain', - component_name + '_audioMain_v001.wav' - ]) + audio_file = repr.get('data', {}).get('path', "") if os.path.exists(audio_file): - context.data["audio"] = { - 'filename': audio_file, - 'enabled': True - } + context.data["audioFile"] = audio_file self.log.info( 'audio_file: {}, has been added to context'.format(audio_file)) else: From 44175bab45ee9be3516d36060d32dae2dcbd2e30 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 May 2020 18:19:15 +0100 Subject: [PATCH 023/131] typo(pype): fixing typo in attribute --- pype/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/lib.py b/pype/lib.py index 2bd18dacff..247611f207 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -644,7 +644,7 @@ def get_subsets(asset_name, if len(repres_out) > 0: output_dict[subset["name"]] = {"version": version_sel, - "representaions": repres_out} + "representations": repres_out} return output_dict From 9b50845cfa9a30303928e9df212b6ba5dd65f74b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 May 2020 18:20:40 +0100 Subject: [PATCH 024/131] feat(global): collecting audio to instance from metadata.json --- pype/plugins/global/publish/collect_rendered_files.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pype/plugins/global/publish/collect_rendered_files.py b/pype/plugins/global/publish/collect_rendered_files.py index 8ecf7ba156..381f6af372 100644 --- a/pype/plugins/global/publish/collect_rendered_files.py +++ b/pype/plugins/global/publish/collect_rendered_files.py @@ -75,6 +75,16 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): data=r) for r in instance.get("representations")] i.data.update(instance) + # add audio if in metadata data + if data.get("audio"): + i.data.update({ + "audio": [{ + "filename": data.get("audio"), + "offset": 0 + }] + }) + self.log.info(f"Adding audio to instance: {i.data['audio']}") + def process(self, context): self._context = context From f2473391b865f5f9a71d865d8de4da3ff2333369 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 May 2020 18:21:30 +0100 Subject: [PATCH 025/131] clean(celaction): arbitrary code --- .../publish/submit_celaction_deadline.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/pype/plugins/celaction/publish/submit_celaction_deadline.py b/pype/plugins/celaction/publish/submit_celaction_deadline.py index 1d7694b1f0..7b4366ca24 100644 --- a/pype/plugins/celaction/publish/submit_celaction_deadline.py +++ b/pype/plugins/celaction/publish/submit_celaction_deadline.py @@ -66,8 +66,7 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): def payload_submit(self, instance, script_path, - render_path, - responce_data=None + render_path ): resolution_width = instance.data["resolutionWidth"] resolution_height = instance.data["resolutionHeight"] @@ -77,9 +76,6 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): output_filename_0 = self.preview_fname(render_path) - if not responce_data: - responce_data = {} - try: # Ensure render folder exists os.makedirs(render_dir) @@ -125,11 +121,11 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): "Department": self.deadline_department, "Priority": self.deadline_priority, - "ChunkSize": chunk_size, "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, @@ -159,14 +155,6 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): "AuxFiles": [] } - if responce_data.get("_id"): - payload["JobInfo"].update({ - "JobType": "Normal", - "BatchName": responce_data["Props"]["Batch"], - "JobDependency0": responce_data["_id"], - "ChunkSize": 99999999 - }) - # Include critical environment variables with submission keys = [ "FTRACK_API_USER", From c51b071a6452341bd6260a6f70d053021607cc81 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 3 Jun 2020 10:55:33 +0100 Subject: [PATCH 026/131] feat(celaction): create workfile from template if none --- pype/celaction/celaction_template_scene.scn | Bin 0 -> 5151 bytes pype/hooks/celaction/prelaunch.py | 50 ++++++++++++++------ 2 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 pype/celaction/celaction_template_scene.scn diff --git a/pype/celaction/celaction_template_scene.scn b/pype/celaction/celaction_template_scene.scn new file mode 100644 index 0000000000000000000000000000000000000000..54e4497a31deb3dcbdac278364f13289386c2b62 GIT binary patch literal 5151 zcmeHLUuauZ7(b~sYqQFrjvAcRJ z+7Q@-h=_uF5fo&0F#N|55kX{uihCN2^+ni&e^Yx%ZxX7ZMNO8I7rt7-MWJxDCubjNw-!&CID8D?68eD9D3pE6qo;Ig@j3 zuJF=)_SqSW@6{gUH4qP5Q?<~h$}VGEI_ON5hGbp6HhL_*I zm^^ggk%5DnrqVihfi-_>%6u_Z$QI08HgBe;t@(83xs+wj7IJCJJetawxwI(!fuor` z~7e5z;b?W9d1?t zSE_X1uiU0F$-j}#fA#qDLA5z*`Y0pWUa*{5I@)I3L-b2XGSDgH$Uq}9K*zEOQ`>k!0 zZ69Cy!0v^94cL3C3C?#7Iti+_TWCw?=BE%p-LYivgXHKeQT{DUwgE{w3GbdWt%K%C z{&ycZZvA+@*QWVeTdZ=$1EGZF+_y>J~$3ps$FVOD@Lk$o4)OgGvG^f)q z1^F9hY7_$;EP76OIOEhf%-(u?+b=T;11=dIA9n= zga(XoY{+M%DGsfko;7Px0c`@w1N}l!8~_B0%~`->@O}{tK@%0rOsEmM(nQI7u*~(& z`nHoxg-2FMxUSv1A|!1Zr=xVZhqfMUU(F{V)n@eT{cf|R-*OdLU0oG>h4xGBlKXa1 z`Rq2vu0B;%_-jSw((Sk%JY7@<;V0Zirc1Mg8^jM{)0Gax0?R$f7{WJEvwP9{H;c+9 zVAWBZ8|F0pYEgLuW1W7os7xZ}P3Xq=&FAJt{D!?F`+ z!v#ojPlhC)y!S#CgohsM2(QsCm}of29Nm4Kce?8BI^p#-Zwaz{e9c>aV$C}MCPWul zj)y0!c-BLnp}B=74OI_DS)agMkz-PrILi`7Q^!LQHOjWb=VhO;9Y`kgQEqX}k3UKw zY&V$twpAsBAc-fV8NLj^8$&q8tg;X+UzLuvxs_|$+|%g!CtST>&V|kqk{WYKi_Rtz zEq%@!M bool: + + from pprint import pformat + self.log.info(f"`{pformat(env)}`") if not env: - self.env = os.environ - else: - self.env = env + env = os.environ + # initialize self._S = api.Session - project = self._S["AVALON_PROJECT"] = self.env["AVALON_PROJECT"] - asset = self._S["AVALON_ASSET"] = self.env["AVALON_ASSET"] - task = self._S["AVALON_TASK"] = self.env["AVALON_TASK"] - workdir = self._S["AVALON_WORKDIR"] = self.env["AVALON_WORKDIR"] - - anatomy_filled = self.get_anatomy_filled() + # get publish version of celaction app = "celaction_publish" + + # get context variables + project = self._S["AVALON_PROJECT"] = env["AVALON_PROJECT"] + asset = self._S["AVALON_ASSET"] = env["AVALON_ASSET"] + task = self._S["AVALON_TASK"] = env["AVALON_TASK"] + workdir = self._S["AVALON_WORKDIR"] = env["AVALON_WORKDIR"] + + # get workfile path + anatomy_filled = self.get_anatomy_filled() workfile = anatomy_filled["work"]["file"] version = anatomy_filled["version"] + # create workdir if doesn't exist os.makedirs(workdir, exist_ok=True) self.log.info(f"Work dir is: `{workdir}`") - # get last version if any + # get last version of workfile workfile_last = get_last_version_from_path( workdir, workfile.split(version)) if workfile_last: workfile = workfile_last - project_file = os.path.join(workdir, workfile) - env["PYPE_CELACTION_PROJECT_FILE"] = project_file + workfile_path = os.path.join(workdir, workfile) - self.log.info(f"Workfile to open: `{project_file}`") + # create workfile from template if doesnt exist any on path + if not os.path.isfile(workfile_path): + # try to get path from environment or use default + # from `pype.celation` dir + template_path = env.get("CELACTION_TEMPLATE") or os.path.join( + env.get("PYPE_MODULE_ROOT"), + "pype/celaction/celaction_template_scene_.scn" + ) + self.log.info(f"Creating workfile from template: `{template_path}`") + shutil.copy2( + os.path.normpath(template_path), + os.path.normpath(workfile_path) + ) + + self.log.info(f"Workfile to open: `{workfile_path}`") - ########################## # setting output parameters path = r"Software\CelAction\CelAction2D\User Settings" winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) @@ -70,7 +90,7 @@ class CelactionPrelaunchHook(PypeHook): winreg.KEY_ALL_ACCESS) # TODO: change to root path and pyblish standalone to premiere way - pype_root_path = os.getenv("PYPE_ROOT") + pype_root_path = os.getenv("PYPE_SETUP_PATH") path = os.path.join(pype_root_path, "pype.bat") From d809d6e4b6671441ce720d29419a0db50d8cf510 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 3 Jun 2020 11:50:32 +0100 Subject: [PATCH 027/131] fix(celaction): missing environment var for opening workfile --- pype/hooks/celaction/prelaunch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/hooks/celaction/prelaunch.py b/pype/hooks/celaction/prelaunch.py index ffd3a78bf3..55956a7481 100644 --- a/pype/hooks/celaction/prelaunch.py +++ b/pype/hooks/celaction/prelaunch.py @@ -65,7 +65,7 @@ class CelactionPrelaunchHook(PypeHook): workfile_path = os.path.join(workdir, workfile) - # create workfile from template if doesnt exist any on path + # copy workfile from template if doesnt exist any on path if not os.path.isfile(workfile_path): # try to get path from environment or use default # from `pype.celation` dir @@ -81,6 +81,9 @@ class CelactionPrelaunchHook(PypeHook): self.log.info(f"Workfile to open: `{workfile_path}`") + # adding compulsory environment var for openting file + env["PYPE_CELACTION_PROJECT_FILE"] = workfile_path + # setting output parameters path = r"Software\CelAction\CelAction2D\User Settings" winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) From 12dee5053be469ec968d1a8eedfedb49e3f331f9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 4 Jun 2020 17:07:22 +0200 Subject: [PATCH 028/131] fix(global): i is not instance as it should --- pype/plugins/global/publish/collect_rendered_files.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/collect_rendered_files.py b/pype/plugins/global/publish/collect_rendered_files.py index bd54204c96..e0f3695fd5 100644 --- a/pype/plugins/global/publish/collect_rendered_files.py +++ b/pype/plugins/global/publish/collect_rendered_files.py @@ -101,13 +101,14 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): # add audio if in metadata data if data.get("audio"): - i.data.update({ + instance.data.update({ "audio": [{ "filename": data.get("audio"), "offset": 0 }] }) - self.log.info(f"Adding audio to instance: {i.data['audio']}") + self.log.info( + f"Adding audio to instance: {instance.data['audio']}") def process(self, context): self._context = context From b663ad42e62303fe039e893fc986a44d1aff2e54 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 4 Jun 2020 17:32:46 +0200 Subject: [PATCH 029/131] fix(global): convert to unc path from drive - also adding PYPE_PYTHON_EXE to submission --- .../plugins/global/publish/submit_publish_job.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index d2d42c7d15..79a02e11cf 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -14,6 +14,8 @@ import pyblish.api def _get_script(): """Get path to the image sequence script.""" + from pathlib import Path + try: from pype.scripts import publish_filesequence except Exception: @@ -23,7 +25,9 @@ def _get_script(): if module_path.endswith(".pyc"): module_path = module_path[: -len(".pyc")] + ".py" - return os.path.normpath(module_path) + path = Path(os.path.normpath(module_path)).resolve(strict=True) + + return str(path) def get_latest_version(asset_name, subset_name, family): @@ -157,7 +161,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "FTRACK_SERVER", "PYPE_METADATA_FILE", "AVALON_PROJECT", - "PYPE_LOG_NO_COLORS" + "PYPE_LOG_NO_COLORS", + "PYPE_PYTHON_EXE" ] # custom deadline atributes @@ -209,6 +214,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): ).format(output_dir)) rootless_path = output_dir + # gets script path + script_path = _get_script() + self.log.info("Adding script path: `{}`...".format(script_path)) + # Generate the payload for Deadline submission payload = { "JobInfo": { @@ -231,7 +240,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): }, "PluginInfo": { "Version": "3.6", - "ScriptFile": _get_script(), + "ScriptFile": script_path, "Arguments": "", "SingleFrameOnly": "True", }, @@ -245,7 +254,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): metadata_path = os.path.join(rootless_path, metadata_filename) environment = job["Props"].get("Env", {}) - environment["PYPE_PYTHON_EXE"] = "//pype/Core/software/python36/python.exe" environment["PYPE_LOG_NO_COLORS"] = "1" environment["PYPE_METADATA_FILE"] = metadata_path environment["AVALON_PROJECT"] = io.Session["AVALON_PROJECT"] From 5606277f6ee0e2253ff37e9fa60ce925ed8f32a6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 4 Jun 2020 17:34:07 +0200 Subject: [PATCH 030/131] clean(global) --- pype/plugins/global/publish/integrate_new.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index bd908901cc..8d7a722c5c 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -375,9 +375,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): index_frame_start += 1 dst = "{0}{1}{2}".format( - dst_head, - dst_padding, - dst_tail).replace("..", ".") + dst_head, + dst_padding, + dst_tail + ).replace("..", ".") self.log.debug("destination: `{}`".format(dst)) src = os.path.join(stagingdir, src_file_name) From 46e07c03544ea261c1d4dd3a9281b5c0e0423973 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 4 Jun 2020 18:10:28 +0200 Subject: [PATCH 031/131] fix(global): if speedcopy fails copy with shutill --- pype/plugins/global/publish/integrate_new.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 8d7a722c5c..fcc4adb256 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -377,8 +377,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst = "{0}{1}{2}".format( dst_head, dst_padding, - dst_tail - ).replace("..", ".") + dst_tail).replace("..", ".") self.log.debug("destination: `{}`".format(dst)) src = os.path.join(stagingdir, src_file_name) @@ -557,10 +556,16 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): while True: try: copyfile(src, dst) - except OSError as e: - self.log.critical("Cannot copy {} to {}".format(src, dst)) - self.log.critical(e) - six.reraise(*sys.exc_info()) + except (OSError, AttributeError) as e: + self.log.warning(e) + # try it again with shutil + import shutil + try: + shutil.copyfile(src, dst) + except (OSError, AttributeError) as e: + self.log.critical("Cannot copy {} to {}".format(src, dst)) + self.log.critical(e) + six.reraise(*sys.exc_info()) if str(getsize(src)) in str(getsize(dst)): break From 1213357d29aadde37f4875dbccd19cc10dad2660 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 4 Jun 2020 18:19:55 +0200 Subject: [PATCH 032/131] feat(global): adding debug print --- pype/plugins/global/publish/integrate_new.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index fcc4adb256..5c30a7b4bc 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -562,6 +562,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): import shutil try: shutil.copyfile(src, dst) + self.log.debug("Copying files with shutil...") except (OSError, AttributeError) as e: self.log.critical("Cannot copy {} to {}".format(src, dst)) self.log.critical(e) From 6664cd963e40dc19c9741261c2c0744ea575fdec Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Jun 2020 18:51:36 +0200 Subject: [PATCH 033/131] input resolutions are converted to integer --- pype/plugins/global/publish/extract_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 141617bda9..97e88243bb 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -610,8 +610,8 @@ class ExtractReview(pyblish.api.InstancePlugin): # NOTE Skipped using instance's resolution full_input_path_single_file = temp_data["full_input_path_single_file"] input_data = pype.lib.ffprobe_streams(full_input_path_single_file)[0] - input_width = input_data["width"] - input_height = input_data["height"] + input_width = int(input_data["width"]) + input_height = int(input_data["height"]) self.log.debug("pixel_aspect: `{}`".format(pixel_aspect)) self.log.debug("input_width: `{}`".format(input_width)) From c620ade4bc45ae25602f870c389343cf0899acd1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 4 Jun 2020 18:51:47 +0200 Subject: [PATCH 034/131] output resolution is converted to int too for sure --- pype/plugins/global/publish/extract_review.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 97e88243bb..89cdce95db 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -631,6 +631,9 @@ class ExtractReview(pyblish.api.InstancePlugin): output_width = input_width output_height = input_height + output_width = int(output_width) + output_height = int(output_height) + self.log.debug( "Output resolution is {}x{}".format(output_width, output_height) ) From 082b7cf5c560f68284bbde6c4879687119323f64 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 4 Jun 2020 18:11:43 +0100 Subject: [PATCH 035/131] feat(global): adding celaction detect for preview tags and families --- pype/plugins/global/publish/extract_review.py | 7 +++++-- pype/plugins/global/publish/submit_publish_job.py | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 228b4cd6f4..6ab834d1c4 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -610,8 +610,8 @@ class ExtractReview(pyblish.api.InstancePlugin): # NOTE Skipped using instance's resolution full_input_path_single_file = temp_data["full_input_path_single_file"] input_data = pype.lib.ffprobe_streams(full_input_path_single_file)[0] - input_width = input_data["width"] - input_height = input_data["height"] + input_width = int(input_data["width"]) + input_height = int(input_data["height"]) self.log.debug("pixel_aspect: `{}`".format(pixel_aspect)) self.log.debug("input_width: `{}`".format(input_width)) @@ -631,6 +631,9 @@ class ExtractReview(pyblish.api.InstancePlugin): output_width = input_width output_height = input_height + output_width = int(output_width) + output_height = int(output_height) + self.log.debug( "Output resolution is {}x{}".format(output_width, output_height) ) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 79a02e11cf..e37444964a 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -490,6 +490,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if bake_render_path: preview = False + if "celaction" in self.hosts: + preview = True + staging = os.path.dirname(list(collection)[0]) success, rootless_staging_dir = ( self.anatomy.find_root_template_from_path(staging) From 1a21abdecf72e992aadec15d4afe840edec11834 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 4 Jun 2020 19:24:21 +0200 Subject: [PATCH 036/131] feat(celaction): new pype structure --- pype/hooks/celaction/prelaunch.py | 2 +- pype/{ => hosts}/celaction/__init__.py | 2 +- .../celaction/celaction_template_scene.scn | Bin pype/{ => hosts}/celaction/cli.py | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename pype/{ => hosts}/celaction/__init__.py (93%) rename pype/{ => hosts}/celaction/celaction_template_scene.scn (100%) rename pype/{ => hosts}/celaction/cli.py (100%) diff --git a/pype/hooks/celaction/prelaunch.py b/pype/hooks/celaction/prelaunch.py index 55956a7481..4aa4237035 100644 --- a/pype/hooks/celaction/prelaunch.py +++ b/pype/hooks/celaction/prelaunch.py @@ -71,7 +71,7 @@ class CelactionPrelaunchHook(PypeHook): # from `pype.celation` dir template_path = env.get("CELACTION_TEMPLATE") or os.path.join( env.get("PYPE_MODULE_ROOT"), - "pype/celaction/celaction_template_scene_.scn" + "pype/hosts/celaction/celaction_template_scene.scn" ) self.log.info(f"Creating workfile from template: `{template_path}`") shutil.copy2( diff --git a/pype/celaction/__init__.py b/pype/hosts/celaction/__init__.py similarity index 93% rename from pype/celaction/__init__.py rename to pype/hosts/celaction/__init__.py index 47e81a9212..8c93d93738 100644 --- a/pype/celaction/__init__.py +++ b/pype/hosts/celaction/__init__.py @@ -1 +1 @@ -kwargs = None +kwargs = None diff --git a/pype/celaction/celaction_template_scene.scn b/pype/hosts/celaction/celaction_template_scene.scn similarity index 100% rename from pype/celaction/celaction_template_scene.scn rename to pype/hosts/celaction/celaction_template_scene.scn diff --git a/pype/celaction/cli.py b/pype/hosts/celaction/cli.py similarity index 100% rename from pype/celaction/cli.py rename to pype/hosts/celaction/cli.py From c01115386d5e8d950b6220e6980cce3f388d35cb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 4 Jun 2020 19:26:57 +0200 Subject: [PATCH 037/131] clean(celaction): before PR brushing --- pype/hooks/celaction/prelaunch.py | 6 ++---- pype/hosts/celaction/cli.py | 4 +--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/pype/hooks/celaction/prelaunch.py b/pype/hooks/celaction/prelaunch.py index 4aa4237035..72900fd8d8 100644 --- a/pype/hooks/celaction/prelaunch.py +++ b/pype/hooks/celaction/prelaunch.py @@ -29,9 +29,6 @@ class CelactionPrelaunchHook(PypeHook): self.signature = "( {} )".format(self.__class__.__name__) def execute(self, *args, env: dict = None) -> bool: - - from pprint import pformat - self.log.info(f"`{pformat(env)}`") if not env: env = os.environ @@ -73,7 +70,8 @@ class CelactionPrelaunchHook(PypeHook): env.get("PYPE_MODULE_ROOT"), "pype/hosts/celaction/celaction_template_scene.scn" ) - self.log.info(f"Creating workfile from template: `{template_path}`") + self.log.info( + f"Creating workfile from template: `{template_path}`") shutil.copy2( os.path.normpath(template_path), os.path.normpath(workfile_path) diff --git a/pype/hosts/celaction/cli.py b/pype/hosts/celaction/cli.py index daa971d8d5..6b0c2eeafa 100644 --- a/pype/hosts/celaction/cli.py +++ b/pype/hosts/celaction/cli.py @@ -2,10 +2,8 @@ import os import sys import copy import argparse -import importlib from avalon import io -import avalon.api from avalon.tools import publish import pyblish.api @@ -53,7 +51,7 @@ def cli(): help=("Height of resolution")) # parser.add_argument("--programDir", - # help=("Directory with celaction program installation")) + # help=("Directory with celaction program installation")) pype.celaction.kwargs = parser.parse_args(sys.argv[1:]).__dict__ From eddbc6c04511680439d551fc7f3f1291c21451a6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 8 Jun 2020 16:10:12 +0100 Subject: [PATCH 038/131] Initial working editorial publishing. --- .../publish/integrate_ftrack_instances.py | 12 +- .../publish/extract_hierarchy_avalon.py | 2 +- .../plugins/harmony/publish/extract_render.py | 6 +- .../publish/collect_shots.py | 125 ++++++++++++++++++ 4 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 pype/plugins/standalonepublisher/publish/collect_shots.py diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py index 11b569fd12..f5d7689678 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py @@ -44,10 +44,14 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): family = instance.data['family'].lower() - asset_type = '' - asset_type = instance.data.get( - "ftrackFamily", self.family_mapping[family] - ) + asset_type = instance.data.get("ftrackFamily") + if not asset_type and family in self.family_mapping: + asset_type = self.family_mapping[family] + + # Ignore this instance if neither "ftrackFamily" or a family mapping is + # found. + if not asset_type: + return componentList = [] ft_session = instance.context.data["ftrackSession"] diff --git a/pype/plugins/global/publish/extract_hierarchy_avalon.py b/pype/plugins/global/publish/extract_hierarchy_avalon.py index ab8226f6ef..83cf03b042 100644 --- a/pype/plugins/global/publish/extract_hierarchy_avalon.py +++ b/pype/plugins/global/publish/extract_hierarchy_avalon.py @@ -7,7 +7,7 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): order = pyblish.api.ExtractorOrder - 0.01 label = "Extract Hierarchy To Avalon" - families = ["clip", "shot"] + families = ["clip", "shot", "editorial"] def process(self, context): if "hierarchyContext" not in context.data: diff --git a/pype/plugins/harmony/publish/extract_render.py b/pype/plugins/harmony/publish/extract_render.py index de6e8b9008..eb0850dc58 100644 --- a/pype/plugins/harmony/publish/extract_render.py +++ b/pype/plugins/harmony/publish/extract_render.py @@ -60,8 +60,10 @@ class ExtractRender(pyblish.api.InstancePlugin): harmony.save_scene() # Execute rendering. - output = pype.lib._subprocess([application_path, "-batch", scene_path]) - self.log.info(output) + import subprocess + subprocess.call([application_path, "-batch", scene_path]) + #output = pype.lib._subprocess([application_path, "-batch", scene_path]) + #self.log.info(output) # Collect rendered files. files = os.listdir(path) diff --git a/pype/plugins/standalonepublisher/publish/collect_shots.py b/pype/plugins/standalonepublisher/publish/collect_shots.py new file mode 100644 index 0000000000..fb566a15ee --- /dev/null +++ b/pype/plugins/standalonepublisher/publish/collect_shots.py @@ -0,0 +1,125 @@ +import os +from urllib.parse import unquote, urlparse + +import opentimelineio as otio +from bson import json_util + +import pyblish.api +from pype import lib +from avalon import io + + +class OTIO_View(pyblish.api.Action): + """Currently disabled because OTIO requires PySide2. Issue on Qt.py: + https://github.com/PixarAnimationStudios/OpenTimelineIO/issues/289 + """ + + label = "OTIO View" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + instance = context[0] + representation = instance.data["representations"][0] + file_path = os.path.join( + representation["stagingDir"], representation["files"] + ) + lib._subprocess(["otioview", file_path]) + + +class CollectShots(pyblish.api.InstancePlugin): + """Collect Anatomy object into Context""" + + order = pyblish.api.CollectorOrder + label = "Collect Shots" + hosts = ["standalonepublisher"] + families = ["editorial"] + actions = [] + + def process(self, instance): + representation = instance.data["representations"][0] + file_path = os.path.join( + representation["stagingDir"], representation["files"] + ) + timeline = otio.adapters.read_from_file(file_path) + tracks = timeline.each_child( + descended_from_type=otio.schema.track.Track + ) + asset_entity = instance.context.data["assetEntity"] + asset_name = asset_entity["name"] + + # Project specific prefix naming. This needs to be replaced with some + # options to be more flexible. + asset_name = asset_name.split("_")[0] + + shot_number = 10 + for track in tracks: + self.log.info(track) + + if "audio" in track.name.lower(): + continue + + instances = [] + for child in track.each_child(): + parse = urlparse(child.media_reference.target_url) + + # XML files from NukeStudio has extra "/" at the front of path. + path = os.path.normpath( + os.path.abspath(unquote(parse.path)[1:]) + ) + + frame_start = child.range_in_parent().start_time.value + frame_end = child.range_in_parent().end_time_inclusive().value + + name = f"{asset_name}_sh{shot_number:04}" + label = f"{name} (framerange: {frame_start}-{frame_end})" + instances.append( + instance.context.create_instance(**{ + "name": name, + "label": label, + "path": path, + "frameStart": frame_start, + "frameEnd": frame_end, + "family": "shot", + "asset": name, + "subset": "shotMain" + }) + ) + + shot_number += 10 + + visual_hierarchy = [asset_entity] + while True: + visual_parent = io.find_one( + {"_id": visual_hierarchy[-1]["data"]["visualParent"]} + ) + if visual_parent: + visual_hierarchy.append(visual_parent) + else: + visual_hierarchy.append(instance.context.data["projectEntity"]) + break + + context_hierarchy = None + for entity in visual_hierarchy: + childs = {} + if context_hierarchy: + name = context_hierarchy.pop("name") + childs = {name: context_hierarchy} + else: + for instance in instances: + childs[instance.data["name"]] = { + "childs": {}, "entity_type": "Shot" + } + + context_hierarchy = { + "entity_type": entity["data"]["entityType"], + "childs": childs, + "name": entity["name"] + } + + name = context_hierarchy.pop("name") + context_hierarchy = {name: context_hierarchy} + instance.context.data["hierarchyContext"] = context_hierarchy + self.log.info( + json_util.dumps(context_hierarchy, sort_keys=True, indent=4) + ) From 14b29fd5ac708c7b25c753f3908f7e7f74a8adba Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 8 Jun 2020 16:17:37 +0100 Subject: [PATCH 039/131] Fix accidental commit. --- pype/plugins/harmony/publish/extract_render.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pype/plugins/harmony/publish/extract_render.py b/pype/plugins/harmony/publish/extract_render.py index eb0850dc58..de6e8b9008 100644 --- a/pype/plugins/harmony/publish/extract_render.py +++ b/pype/plugins/harmony/publish/extract_render.py @@ -60,10 +60,8 @@ class ExtractRender(pyblish.api.InstancePlugin): harmony.save_scene() # Execute rendering. - import subprocess - subprocess.call([application_path, "-batch", scene_path]) - #output = pype.lib._subprocess([application_path, "-batch", scene_path]) - #self.log.info(output) + output = pype.lib._subprocess([application_path, "-batch", scene_path]) + self.log.info(output) # Collect rendered files. files = os.listdir(path) From b8dde529b24c2bf0ecc8c9eca090c04128438065 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 Jun 2020 08:32:04 +0200 Subject: [PATCH 040/131] fix(celaction, global): PR comments --- pype/hooks/celaction/prelaunch.py | 7 +- pype/hosts/celaction/cli.py | 10 +- .../append_celaction_ftrack_asset_name.py | 19 -- .../append_celaction_ftrack_data.py | 46 --- .../collect_celaction_scene.py | 10 - .../celaction/_unused_publish/collect_data.py | 18 -- .../_unused_publish/collect_render_path.py | 60 ---- .../extract_celaction_deadline_old.py | 109 ------- .../_unused_publish/submit_publish_job.py | 283 ------------------ .../validate_celaction_scene_path.py | 62 ---- .../celaction/publish/collect_audio.py | 1 - .../celaction/publish/collect_render_path.py | 4 +- .../publish/submit_celaction_deadline.py | 20 -- pype/plugins/global/publish/integrate_new.py | 15 +- .../global/publish/submit_publish_job.py | 2 +- 15 files changed, 16 insertions(+), 650 deletions(-) delete mode 100644 pype/plugins/celaction/_unused_publish/append_celaction_ftrack_asset_name.py delete mode 100644 pype/plugins/celaction/_unused_publish/append_celaction_ftrack_data.py delete mode 100644 pype/plugins/celaction/_unused_publish/collect_celaction_scene.py delete mode 100644 pype/plugins/celaction/_unused_publish/collect_data.py delete mode 100644 pype/plugins/celaction/_unused_publish/collect_render_path.py delete mode 100644 pype/plugins/celaction/_unused_publish/extract_celaction_deadline_old.py delete mode 100644 pype/plugins/celaction/_unused_publish/submit_publish_job.py delete mode 100644 pype/plugins/celaction/_unused_publish/validate_celaction_scene_path.py diff --git a/pype/hooks/celaction/prelaunch.py b/pype/hooks/celaction/prelaunch.py index 72900fd8d8..df9da6cbbf 100644 --- a/pype/hooks/celaction/prelaunch.py +++ b/pype/hooks/celaction/prelaunch.py @@ -3,8 +3,11 @@ import os import winreg import shutil from pype.lib import PypeHook -from pype.api import get_last_version_from_path -from pypeapp import Anatomy, Logger +from pype.api import ( + Anatomy, + Logger, + get_last_version_from_path +) from avalon import io, api, lib diff --git a/pype/hosts/celaction/cli.py b/pype/hosts/celaction/cli.py index 6b0c2eeafa..fa55db3200 100644 --- a/pype/hosts/celaction/cli.py +++ b/pype/hosts/celaction/cli.py @@ -9,23 +9,19 @@ from avalon.tools import publish import pyblish.api import pyblish.util -from pypeapp import Logger +from pype.api import Logger import pype import pype.celaction - log = Logger().get_logger("Celaction_cli_publisher") publish_host = "celaction" -CURRENT_DIR = os.path.dirname(__file__) -PACKAGE_DIR = os.path.dirname(CURRENT_DIR) -PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") -PUBLISH_PATH = os.path.join(PLUGINS_DIR, publish_host, "publish") +PUBLISH_PATH = os.path.join(pype.PLUGINS_DIR, publish_host, "publish") PUBLISH_PATHS = [ PUBLISH_PATH, - os.path.join(PLUGINS_DIR, "ftrack", "publish") + os.path.join(pype.PLUGINS_DIR, "ftrack", "publish") ] diff --git a/pype/plugins/celaction/_unused_publish/append_celaction_ftrack_asset_name.py b/pype/plugins/celaction/_unused_publish/append_celaction_ftrack_asset_name.py deleted file mode 100644 index 175a19edd3..0000000000 --- a/pype/plugins/celaction/_unused_publish/append_celaction_ftrack_asset_name.py +++ /dev/null @@ -1,19 +0,0 @@ -import pyblish.api - - -class AppendCelactionFtrackAssetName(pyblish.api.InstancePlugin): - """ Appending "ftrackAssetName" """ - - label = "Ftrack Asset Name" - order = pyblish.api.CollectorOrder + 0.1 - - def process(self, instance): - - # skipping if not launched from ftrack - if "ftrackData" not in instance.context.data: - return - - ftrack_data = instance.context.data["ftrackData"] - - asset_name = ftrack_data["Task"]["name"] - instance.data["ftrackAssetName"] = asset_name diff --git a/pype/plugins/celaction/_unused_publish/append_celaction_ftrack_data.py b/pype/plugins/celaction/_unused_publish/append_celaction_ftrack_data.py deleted file mode 100644 index 552f4ffb1a..0000000000 --- a/pype/plugins/celaction/_unused_publish/append_celaction_ftrack_data.py +++ /dev/null @@ -1,46 +0,0 @@ -import pyblish.api -from bait.ftrack.query_runner import QueryRunner - - -class AppendCelactionFtrackAudio(pyblish.api.ContextPlugin): - - label = "Ftrack Audio" - order = pyblish.api.ExtractorOrder - - def process(self, context): - - if context.data.get("audio", ''): - self.log.info('Audio data are already collected') - self.log.info('Audio: {}'.format(context.data.get("audio", ''))) - return - - runner = QueryRunner(context.data['ftrackSession']) - - audio_file = runner.get_audio_file_for_shot( - context.data['ftrackData']["Shot"]["id"]) - - if audio_file: - context.data["audio"] = { - 'filename': audio_file, - 'enabled': True - } - else: - self.log.warning("Couldn't find any audio file on Ftrack.") - - -class AppendCelactionFtrackData(pyblish.api.InstancePlugin): - """ Appending ftrack component and asset type data """ - - families = ["img.*", "mov.*"] - # offset to piggy back from default collectors - order = pyblish.api.CollectorOrder + 0.1 - - def process(self, instance): - - # ftrack data - if not instance.context.has_data("ftrackData"): - return - - instance.data["ftrackComponents"] = {} - asset_type = instance.data["family"].split(".")[0] - instance.data["ftrackAssetType"] = asset_type diff --git a/pype/plugins/celaction/_unused_publish/collect_celaction_scene.py b/pype/plugins/celaction/_unused_publish/collect_celaction_scene.py deleted file mode 100644 index 006edcb1c3..0000000000 --- a/pype/plugins/celaction/_unused_publish/collect_celaction_scene.py +++ /dev/null @@ -1,10 +0,0 @@ -import pyblish.api - - -class CollectCelactionScene(pyblish.api.ContextPlugin): - """ Converts the path flag value to the current file in the context. """ - - order = pyblish.api.CollectorOrder - - def process(self, context): - context.data['ftrackStatus'] = "Ready" diff --git a/pype/plugins/celaction/_unused_publish/collect_data.py b/pype/plugins/celaction/_unused_publish/collect_data.py deleted file mode 100644 index ce416403cc..0000000000 --- a/pype/plugins/celaction/_unused_publish/collect_data.py +++ /dev/null @@ -1,18 +0,0 @@ -import os -import pyblish.api -import pype.celaction - - -class CollectData(pyblish.api.Collector): - """Collects data passed from via CLI""" - - order = pyblish.api.Collector.order - 0.1 - - def process(self, context): - self.log.info("Adding data from command-line into Context..") - - kwargs = pype.celaction.kwargs.copy() - - for key, value in kwargs.items(): - self.log.info("%s = %s" % (key, value)) - context.set_data(key, value) diff --git a/pype/plugins/celaction/_unused_publish/collect_render_path.py b/pype/plugins/celaction/_unused_publish/collect_render_path.py deleted file mode 100644 index 72adf57e86..0000000000 --- a/pype/plugins/celaction/_unused_publish/collect_render_path.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Requires: - context -> anatomy - context -> anatomyData - -Provides: - instance -> publishDir - instance -> resourcesDir -""" - -import os -import copy - -import pyblish.api -from avalon import api - - -class CollectRenderPath(pyblish.api.InstancePlugin): - """Generate file and directory path where rendered images will be""" - - label = "Collect Render Path" - order = pyblish.api.CollectorOrder + 0.495 - - def process(self, instance): - anatomy = instance.context.data["anatomy"] - - template_data = copy.deepcopy(instance.data["anatomyData"]) - - # This is for cases of Deprecated anatomy without `folder` - # TODO remove when all clients have solved this issue - template_data.update({ - "frame": "FRAME_TEMP", - "representation": "png" - }) - - anatomy_filled = anatomy.format(template_data) - - if "folder" in anatomy.templates["render"]: - render_folder = anatomy_filled["render"]["folder"] - render_file = anatomy_filled["render"]["file"] - else: - # solve deprecated situation when `folder` key is not underneath - # `publish` anatomy - project_name = api.Session["AVALON_PROJECT"] - self.log.warning(( - "Deprecation warning: Anatomy does not have set `folder`" - " key underneath `publish` (in global of for project `{}`)." - ).format(project_name)) - - file_path = anatomy_filled["render"]["path"] - # Directory - render_folder = os.path.dirname(file_path) - render_file = os.path.basename(file_path) - - render_folder = os.path.normpath(render_folder) - render_path = os.path.join(render_folder, render_file) - - instance.data["outputRenderPath"] = render_path - - self.log.debug("outputRenderPath: \"{}\"".format(render_path)) diff --git a/pype/plugins/celaction/_unused_publish/extract_celaction_deadline_old.py b/pype/plugins/celaction/_unused_publish/extract_celaction_deadline_old.py deleted file mode 100644 index 322bb468f9..0000000000 --- a/pype/plugins/celaction/_unused_publish/extract_celaction_deadline_old.py +++ /dev/null @@ -1,109 +0,0 @@ -import os - -import pyblish.api -import pyblish_standalone -import clique -import requests -from bait.deadline import get_render_settings, get_deadline_data, format_frames -from bait.paths import get_output_path - - -class ExtractCelactionDeadline(pyblish.api. InstancePlugin): - - label = 'Deadline' - families = ['render'] - order = pyblish.api.ExtractorOrder - - def process(self, instance): - - render_settings = get_render_settings("celaction") - - existing_data = instance.data.get( - "deadlineData", {"job": {}, "plugin": {}} - ) - - task_id = instance.context.data["ftrackData"]["Task"]["id"] - - data = get_deadline_data(render_settings, existing_data) - - filename = os.path.basename(instance.context.data["currentFile"]) - filename_no_ext, ext = os.path.splitext(filename) - - data["job"]["Name"] = filename_no_ext + " - " + instance.data["name"] - data["job"]['Frames'] = format_frames( - instance.data('start'), instance.data('end')) - - # get version data - version = instance.context.data( - 'version') if instance.context.has_data('version') else 1 - - output_path = get_output_path( - task_id, instance.data["name"], version, "png") - output_path = output_path.replace("/", "\\") - - data['job']['Plugin'] = 'CelAction' - data['job']["BatchName"] = filename - data['job']["UserName"] = os.environ['USERNAME'] - data["job"]['OutputFilename0'] = output_path.replace('%04d', '####') - - scene_path = pyblish_standalone.kwargs['path'][0] - scene_path = scene_path.replace("/", "\\") - _, ext = os.path.splitext(scene_path) - - # plugin data - self.log.info(scene_path) - - args = '{}'.format(scene_path) - args += ' -a' - args += ' -8' - args += ' -s ' - args += ' -e ' - args += ' -d {}'.format(os.path.dirname(output_path)) - args += ' -x {}'.format(instance.data('x')) - args += ' -y {}'.format(instance.data('y')) - args += ' -r {}'.format(output_path.replace('.%04d', '')) - args += ' -= AbsoluteFrameNumber=on -= PadDigits=4' - args += ' -= ClearAttachment=on' - - data["plugin"]['StartupDirectory'] = '' - data["plugin"]['Arguments'] = args - - self.log.info(data) - - head = output_path.replace('%04d', '') - tail = ".png" - collection = clique.Collection(head=head, padding=4, tail=tail) - - frame_start = int(instance.data['start']) - frame_end = int(instance.data['end']) - - for frame_no in range(frame_start, frame_end): - collection.add(head + str(frame_no).zfill(4) + tail) - - # instance.data["collection"] = collection - - # adding to instance - instance.set_data('deadlineData2', value=data) - # instance.set_data('deadlineSubmissionJob', value=data) - - payload = { - "JobInfo": data["job"], - "PluginInfo": data["plugin"], - "AuxFiles": [] - } - - url = "{}/api/jobs".format('http://192.168.146.8:8082') - response = requests.post(url, json=payload) - if not response.ok: - raise Exception(response.text) - - # Store output dir for unified publisher (filesequence) - instance.data["outputDir"] = os.path.dirname( - data["job"]['OutputFilename0']) - instance.data["deadlineSubmissionJob"] = response.json() - - instance.context.data['ftrackStatus'] = "Render" - - # creating output path - if not os.path.exists(os.path.dirname(output_path)): - os.makedirs(os.path.dirname(output_path)) diff --git a/pype/plugins/celaction/_unused_publish/submit_publish_job.py b/pype/plugins/celaction/_unused_publish/submit_publish_job.py deleted file mode 100644 index 2767378b4a..0000000000 --- a/pype/plugins/celaction/_unused_publish/submit_publish_job.py +++ /dev/null @@ -1,283 +0,0 @@ -import os -import json -import pprint -import re -import requests -import pyblish.api - - -class SubmitDependentImageSequenceJobDeadline(pyblish.api.InstancePlugin): - """Submit image sequence publish jobs to Deadline. - - These jobs are dependent on a deadline job submission prior to this - plug-in. - - Renders are submitted to a Deadline Web Service as - supplied via the environment variable DEADLINE_REST_URL - - Options in instance.data: - - deadlineSubmission (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" - - This requires a "startFrame" and "endFrame" to be present in instance.data - or in context.data. - - """ - - label = "Submit image sequence jobs to Deadline" - order = pyblish.api.IntegratorOrder + 0.2 - - hosts = ["celaction"] - - families = [ - "render", - "deadline" - ] - - def process(self, instance): - - # DEADLINE_REST_URL = os.environ.get("DEADLINE_REST_URL", - # "http://localhost:8082") - # assert DEADLINE_REST_URL, "Requires DEADLINE_REST_URL" - - # Get a submission job - - job = instance.data.get("deadlineSubmissionJob") - if not job: - raise RuntimeError("Can't continue without valid deadline " - "submission prior to this plug-in.") - ################ - ft_data = instance.context.data["ftrackData"] - project = ft_data['Project']['name'] - project_code = ft_data['Project']['code'] - projects_path = os.path.dirname(ft_data['Project']['root']) - - data = instance.data.copy() - asset = instance.context.data["ftrackData"]['Shot']['name'] - subset = 'render' + \ - instance.context.data["ftrackData"]['Task']['name'].capitalize() - - state = data.get("publishJobState", "Suspended") - # job_name = "{batch} - {subset} [publish image sequence]".format( - # batch=job["Props"]["Name"], - # subset=subset - # ) - job_name = "{asset} [publish image sequence]".format( - asset=asset - ) - - # Get start/end frame from instance, if not available get from context - context = instance.context - - start = int(instance.data['start']) - end = int(instance.data['end']) - - try: - source = data['source'] - except KeyError: - source = context.data["currentFile"] - - # Write metadata for publish job - render_job = data.pop("deadlineSubmissionJob") - metadata = { - "asset": asset, - "regex": r"^.*\.png", - "subset": subset, - "startFrame": start, - "endFrame": end, - "fps": context.data.get("fps", None), - "families": ["render"], - "source": source, - "user": context.data["user"], - "version": context.data.get('version'), - "audio": context.data["audio"]['filename'], - # Optional metadata (for debugging) - "metadata": { - "instance": data, - "job": job, - "session": fake_avalon_session(project, projects_path) - } - } - - # Ensure output dir exists - output_dir = instance.data["outputDir"] - - if not os.path.isdir(output_dir): - os.makedirs(output_dir) - - for k, v in metadata.items(): - self.log.info(k) - self.log.info(v) - - metadata_filename = "{}_metadata.json".format(subset) - metadata_path = os.path.join(output_dir, metadata_filename) - with open(metadata_path, "w") as f: - json.dump(metadata, f, indent=4, sort_keys=True) - - # Generate the payload for Deadline submission - payload = { - "JobInfo": { - "Plugin": "Python", - "BatchName": job["Props"]["Batch"], - "Name": job_name, - "JobType": "Normal", - "Group": "celaction", - "JobDependency0": job["_id"], - "UserName": os.environ['USERNAME'], - "Comment": instance.context.data.get("comment", ""), - "InitialStatus": "Active" - }, - "PluginInfo": { - "Version": "3.6", - "ScriptFile": r"\\pype\Core\dev\pype-setup\repos\pype-config\pype\scripts\publish_filesequence.py", - "Arguments": '--path "{}"'.format(metadata_path), - "SingleFrameOnly": "True" - }, - - # Mandatory for Deadline, may be empty - "AuxFiles": [] - } - - # Transfer the environment from the original job to this dependent - # job so they use the same environment - environment = fake_env() - environment["AVALON_ASSET"] = asset - environment["AVALON_TASK"] = instance.context.data["ftrackData"]['Task']['name'] - environment["AVALON_PROJECT"] = project - environment["AVALON_PROJECTS"] = projects_path - environment["PYPE_STUDIO_PROJECTS_PUBLISH"] = ft_data['Project']['root'] - environment["PYPE_STUDIO_PROJECTS_RENDER"] = ft_data['Project']['root'] - environment["PYPE_STUDIO_PROJECTS_RESOURCES"] = ft_data['Project']['root'] - environment["PYPE_STUDIO_PROJECTS_WORK"] = ft_data['Project']['root'] - - payload["JobInfo"].update({ - "EnvironmentKeyValue%d" % index: "{key}={value}".format( - key=key, - value=environment[key] - ) for index, key in enumerate(environment) - }) - - # Avoid copied pools and remove secondary pool - payload["JobInfo"]["Pool"] = "animation_2d" - payload["JobInfo"].pop("SecondaryPool", None) - - self.log.info("Submitting..") - # self.log.info(json.dumps(payload, indent=4, sort_keys=True)) - - ################ - ###################### - fake_instance = instance.context.create_instance( - name=(str(instance) + "1")) - - for k, v in data.items(): - self.log.info(k) - fake_instance.data[k] = v - - # fake_instance.data['deadlineData'] = payload - # 'http://192.168.146.8:8082' - url = "{}/api/jobs".format('http://192.168.146.8:8082') - response = requests.post(url, json=payload) - if not response.ok: - raise Exception(response.text) - ###################### - ####################### - - -def fake_avalon_session(project=None, projects_path=None): - return { - "AVALON_APP": "premiere", - "AVALON_APP_VERSION": "2019", - "AVALON_ASSET": "editorial", - "AVALON_CONFIG": "pype", - "AVALON_CONTAINER_ID": "avalon.container", - "AVALON_DB": "Pype", - "AVALON_DEADLINE": "http://192.168.146.8:8082", - "AVALON_DEBUG": "1", - "AVALON_EARLY_ADOPTER": "1", - "AVALON_INSTANCE_ID": "avalon.instance", - "AVALON_LABEL": "Avalon", - "AVALON_LOCATION": "http://127.0.0.1", - "AVALON_MONGO": "mongodb://PypeAdmin:X34vkuwL4wbK9A7X@192.168.146.24:27072/Pype", - "AVALON_PASSWORD": "secret", - "AVALON_PROJECT": project or "LBB2_dev", - "AVALON_PROJECTS": projects_path or "L:/PYPE_test", - "AVALON_SILO": "editorial", - "AVALON_TASK": "conform", - "AVALON_TIMEOUT": "1000", - "AVALON_USERNAME": "avalon", - "AVALON_WORKDIR": "L:/PYPE_test/episodes/editorial/work/conform", - "schema": "avalon-core:session-1.0" - } - - -def fake_env(): - return { - "AVALON_CONFIG": "pype", - "AVALON_CONTAINER_ID": "avalon.container", - "AVALON_CORE": "\\\\pype\\Core\\dev\\pype-setup\\repos\\avalon-core", - "AVALON_DB": "Pype", - "AVALON_DB_DATA": "\\\\pype\\Core\\dev\\mongo_db_data", - "AVALON_DEADLINE": "http://192.168.146.8:8082", - "AVALON_DEBUG": "1", - "AVALON_EARLY_ADOPTER": "1", - "AVALON_ENV_NAME": "pype_env", - "AVALON_HIERARCHY": "", - "AVALON_INSTANCE_ID": "avalon.instance", - "AVALON_LABEL": "Avalon", - "AVALON_LAUNCHER": "\\\\pype\\Core\\dev\\pype-setup\\repos\\avalon-launcher", - "AVALON_LOCATION": "http://127.0.0.1", - "AVALON_MONGO": "mongodb://PypeAdmin:X34vkuwL4wbK9A7X@192.168.146.24:27072/Pype", - "AVALON_MONGO_PORT": "27072", - "AVALON_PASSWORD": "secret", - "AVALON_SCHEMA": "\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates\\schema", - "AVALON_SILO": "", - "AVALON_TIMEOUT": "1000", - "AVALON_USERNAME": "avalon", - "AVALON_WORKDIR": "default", - "DEADLINE_PATH": "C:\\Program Files\\Thinkbox\\Deadline10\\bin", - "DEADLINE_REST_URL": "http://192.168.146.8:8082", - "FTRACK_API_KEY": "NGI0ZGU3ZjMtNzNiZC00NGVlLWEwY2EtMzA1OWJlZGM0MjAyOjozZWZmMThjZi04MjkwLTQxMzQtODUwMC03NTZhMGJiZTM2MTA", - "FTRACK_API_USER": "license@clothcatanimation.com", - "FTRACK_SERVER": "https://clothcat2.ftrackapp.com", - "MONGO_DB_ENTRYDB": "Pype", - "MONGO_DB_PASS": "X34vkuwL4wbK9A7X", - "MONGO_DB_USER": "PypeAdmin", - "PATH": "\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\lib\\site-packages\\PyQt5\\Qt\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\Scripts;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\Library\\bin;\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env\\python3\\Library;;\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates\\bin;\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates\\bin\\windows;\\\\pype\\Core\\dev\\pype-setup\\app;\\\\pype\\core\\software\\ffmpeg\\bin;\\\\pype\\Core\\dev\\Applications\\djv\\bin", - "PYBLISHPLUGINPATH": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pype-config\\pype\\plugins\\ftrack\\publish;", - "PYBLISH_BASE": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-base", - "PYBLISH_HOSTS": "shell", - "PYBLISH_LITE": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-lite", - "PYBLISH_QML": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-qml", - "PYPE_APP_ROOT": "\\\\pype\\Core\\dev\\pype-setup\\app", - "PYPE_DEBUG": "3", - "PYPE_DEBUG_STDOUT": "0", - "PYPE_SETUP_ROOT": "\\\\pype\\Core\\dev\\pype-setup", - "PYPE_STUDIO_CODE": "CC", - "PYPE_STUDIO_CONFIG": "\\\\pype\\Core\\dev\\pype-setup\\repos\\pype-config", - "PYPE_STUDIO_CORE": "\\\\pype\\Core\\dev", - "PYPE_STUDIO_CORE_MOUNT": "\\\\pype\\Core\\dev", - "PYPE_STUDIO_NAME": "Cloth Cat", - "PYPE_STUDIO_SOFTWARE": "\\\\pype\\Core\\dev\\Applications", - "PYPE_STUDIO_TEMPLATES": "\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates", - "PYPE_STUDIO_TOOLS": "\\\\pype\\Core\\dev\\production\\tools", - "PYTHONPATH": "\\\\pype\\Core\\dev\\pype-setup;\\\\pype\\Core\\dev\\pype-setup\\app\\vendor;\\\\pype\\Core\\dev\\pype-setup\\repos\\avalon-core;\\\\pype\\Core\\dev\\pype-setup\\repos\\avalon-launcher;\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-base;\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-qml;\\\\pype\\Core\\dev\\pype-setup\\repos\\pyblish-lite;\\\\pype\\Core\\dev\\pype-setup\\repos\\ftrack-event-server;\\\\pype\\Core\\dev\\pype-setup\\repos\\pype-config;\\\\pype\\Core\\dev\\pype-setup\\app\\vendor;\\\\pype\\Core\\dev\\pype-setup\\repos\\pype-config\\pype\\vendor;\\\\pype\\Core\\dev\\pype-setup\\repos\\ftrack-event-server", - "PYTHONVERBOSE": "True", - "PYTHON_ENV": "C:\\Users\\Public\\pype_env", - "REMOTE_ENV_DIR": "\\\\pype\\Core\\dev\\pype-setup\\bin\\python\\pype_env", - "REMOTE_ENV_ON": "0", - "SCHEMA": "avalon-core:session-1.0", - "STUDIO_SOFT": "\\\\evo2\\core\\Applications", - "TOOL_ENV": "\\\\pype\\Core\\dev\\pype-setup\\repos\\clothcat-templates\\environments", - "USERNAME": "pype" - } diff --git a/pype/plugins/celaction/_unused_publish/validate_celaction_scene_path.py b/pype/plugins/celaction/_unused_publish/validate_celaction_scene_path.py deleted file mode 100644 index 17365178ac..0000000000 --- a/pype/plugins/celaction/_unused_publish/validate_celaction_scene_path.py +++ /dev/null @@ -1,62 +0,0 @@ -import shutil -import pyblish.api -import pyblish_standalone -import os -from bait.paths import get_env_work_file - - -class RepairCelactionScenePath(pyblish.api.Action): - label = "Repair" - icon = "wrench" - on = "failed" - - def process(self, context, plugin): - - # get version data - version = context.data('version') if context.has_data('version') else 1 - - task_id = context.data["ftrackData"]["Task"]["id"] - expected_path = get_env_work_file( - "celaction", task_id, "scn", version).replace('\\\\\\', '\\\\') - - src = context.data["currentFile"] - - if not os.path.exists(os.path.dirname(expected_path)): - os.makedirs(os.path.dirname(expected_path)) - - if os.path.exists(os.path.dirname(expected_path)): - self.log.info("existing to \"%s\"" % expected_path) - - if os.path.exists(expected_path) and ('v001' in expected_path): - os.remove(expected_path) - - shutil.copy2(src, expected_path) - - pyblish_standalone.kwargs['path'] = [expected_path] - context.data["currentFile"] = expected_path - - self.log.info("Saved to \"%s\"" % expected_path) - - -class ValidateCelactionScenePath(pyblish.api.InstancePlugin): - order = pyblish.api.ValidatorOrder - families = ['scene'] - label = 'Scene Path' - actions = [RepairCelactionScenePath] - - def process(self, instance): - - # getting current work file - current_scene_path = pyblish_standalone.kwargs['path'][0] - - version = instance.context.data( - 'version') if instance.context.has_data('version') else 1 - - task_id = instance.context.data["ftrackData"]["Task"]["id"] - expected_scene_path = get_env_work_file( - "celaction", task_id, "scn", version).replace('\\\\\\', '\\\\') - - msg = 'Scene path is not correct: Current: {}, Expected: {}'.format( - current_scene_path, expected_scene_path) - - assert expected_scene_path == current_scene_path, msg diff --git a/pype/plugins/celaction/publish/collect_audio.py b/pype/plugins/celaction/publish/collect_audio.py index 44b40f046c..89fe6bf966 100644 --- a/pype/plugins/celaction/publish/collect_audio.py +++ b/pype/plugins/celaction/publish/collect_audio.py @@ -2,7 +2,6 @@ import pyblish.api import os import pype.api as pype -from avalon import io from pprint import pformat class AppendCelactionAudio(pyblish.api.ContextPlugin): diff --git a/pype/plugins/celaction/publish/collect_render_path.py b/pype/plugins/celaction/publish/collect_render_path.py index 32a4bf7c32..3d092ccf22 100644 --- a/pype/plugins/celaction/publish/collect_render_path.py +++ b/pype/plugins/celaction/publish/collect_render_path.py @@ -13,13 +13,13 @@ class CollectRenderPath(pyblish.api.InstancePlugin): anatomy = instance.context.data["anatomy"] current_file = instance.context.data["currentFile"] work_dir = os.path.dirname(current_file) - work_file = os.path.basename(current_file) padding = anatomy.templates.get("frame_padding", 4) render_dir = os.path.join( work_dir, "render", "celaction" ) render_path = os.path.join( - render_dir, ".".join([instance.data["subset"], f"%0{padding}d", "png"]) + render_dir, + ".".join([instance.data["subset"], f"%0{padding}d", "png"]) ) # create dir if it doesnt exists diff --git a/pype/plugins/celaction/publish/submit_celaction_deadline.py b/pype/plugins/celaction/publish/submit_celaction_deadline.py index 7b4366ca24..0bb346f7cf 100644 --- a/pype/plugins/celaction/publish/submit_celaction_deadline.py +++ b/pype/plugins/celaction/publish/submit_celaction_deadline.py @@ -2,7 +2,6 @@ import os import json import getpass -from avalon import api from avalon.vendor import requests import re import pyblish.api @@ -29,8 +28,6 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): deadline_chunk_size = 1 def process(self, instance): - families = instance.data["families"] - context = instance.context DEADLINE_REST_URL = os.environ.get("DEADLINE_REST_URL") @@ -155,23 +152,6 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): "AuxFiles": [] } - # Include critical environment variables with submission - keys = [ - "FTRACK_API_USER", - "FTRACK_API_KEY", - "FTRACK_SERVER", - "AVALON_PROJECT" - ] - environment = dict({key: os.environ[key] for key in keys - if key in os.environ}) - - 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.info("using render plugin : {}".format(plugin)) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 16785178e1..6c9caf7ab9 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -9,7 +9,7 @@ import six from pymongo import DeleteOne, InsertOne import pyblish.api -from avalon import io +from avalon import api, io from avalon.vendor import filelink # this is needed until speedcopy for linux is fixed @@ -44,7 +44,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "frameStart" "frameEnd" 'fps' - "data": additional metadata for each representation. """ label = "Integrate Asset New" @@ -82,8 +81,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "assembly", "fbx", "textures", - "action", - "harmony.template" + "action" ] exclude_families = ["clip"] db_representation_context_keys = [ @@ -379,8 +377,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst = "{0}{1}{2}".format( dst_head, dst_padding, - dst_tail - ).replace("..", ".") + dst_tail).replace("..", ".") self.log.debug("destination: `{}`".format(dst)) src = os.path.join(stagingdir, src_file_name) @@ -453,15 +450,13 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if repre_id is None: repre_id = io.ObjectId() - data = repre.get("data") or {} - data.update({'path': dst, 'template': template}) representation = { "_id": repre_id, "schema": "pype:representation-2.0", "type": "representation", "parent": version_id, "name": repre['name'], - "data": data, + "data": {'path': dst, 'template': template}, "dependencies": instance.data.get("dependencies", "").split(), # Imprint shortcut to context @@ -568,7 +563,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): try: shutil.copyfile(src, dst) self.log.debug("Copying files with shutil...") - except (OSError, AttributeError) as e: + except (OSError) as e: self.log.critical("Cannot copy {} to {}".format(src, dst)) self.log.critical(e) six.reraise(*sys.exc_info()) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index e37444964a..6da459690d 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -845,7 +845,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # add audio to metadata file if available audio_file = context.data.get("audioFile") if os.path.isfile(audio_file): - publish_job.update({ "audio": audio_file}) + publish_job.update({"audio": audio_file}) # pass Ftrack credentials in case of Muster if submission_type == "muster": From 43730acc3974c04da6ef05ff46c55bfe039e407c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 Jun 2020 08:36:30 +0200 Subject: [PATCH 041/131] fix(global): Hound suggestions --- pype/plugins/global/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 6c9caf7ab9..2bb7ba448e 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -9,7 +9,7 @@ import six from pymongo import DeleteOne, InsertOne import pyblish.api -from avalon import api, io +from avalon import io from avalon.vendor import filelink # this is needed until speedcopy for linux is fixed From e17b7cb59398ba07dc77f16a7942a48772675572 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 10 Jun 2020 18:11:11 +0200 Subject: [PATCH 042/131] ignore empty action key in entity's data --- pype/modules/ftrack/events/event_first_version_status.py | 2 +- pype/modules/ftrack/events/event_thumbnail_updates.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/modules/ftrack/events/event_first_version_status.py b/pype/modules/ftrack/events/event_first_version_status.py index 511907a048..8754d092ab 100644 --- a/pype/modules/ftrack/events/event_first_version_status.py +++ b/pype/modules/ftrack/events/event_first_version_status.py @@ -158,7 +158,7 @@ class FirstVersionStatus(BaseEvent): filtered_ents = [] for entity in event["data"].get("entities", []): # Care only about add actions - if entity["action"] != "add": + if entity.get("action") != "add": continue # Filter AssetVersions diff --git a/pype/modules/ftrack/events/event_thumbnail_updates.py b/pype/modules/ftrack/events/event_thumbnail_updates.py index c33ee08c2d..1304dc8b5e 100644 --- a/pype/modules/ftrack/events/event_thumbnail_updates.py +++ b/pype/modules/ftrack/events/event_thumbnail_updates.py @@ -6,6 +6,9 @@ class ThumbnailEvents(BaseEvent): """Updates thumbnails of entities from new AssetVersion.""" for entity in event["data"].get("entities", []): + action = entity.get("action") + if not action: + continue if ( entity["action"] == "remove" or entity["entityType"].lower() != "assetversion" From 192e983649bf228860cf339414dd95c9fbff53cc Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 10 Jun 2020 18:15:39 +0200 Subject: [PATCH 043/131] Update integrate_new.py return removed family --- pype/plugins/global/publish/integrate_new.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 2bb7ba448e..82d60ae5ae 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -81,7 +81,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "assembly", "fbx", "textures", - "action" + "action", + "harmony.template" ] exclude_families = ["clip"] db_representation_context_keys = [ From 582bda9fad3f8c407633a0896a869ea98349f4af Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 10 Jun 2020 20:21:25 +0100 Subject: [PATCH 044/131] Fix harmony plugin paths. --- pype/hosts/harmony/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index b3edca7d15..765dcb9e72 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -8,7 +8,9 @@ def install(): print("Installing Pype config...") plugins_directory = os.path.join( - os.path.dirname(os.path.dirname(__file__)), "plugins", "harmony" + os.path.dirname(os.path.dirname(os.path.dirname(__file__))), + "plugins", + "harmony" ) pyblish.api.register_plugin_path( From 5ca610941ba70a2288fe30f59a24cc21e2b139bf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 11 Jun 2020 16:24:07 +0200 Subject: [PATCH 045/131] fixed pyblish gui setup in standalone publisher --- pype/modules/standalonepublish/publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/standalonepublish/publish.py b/pype/modules/standalonepublish/publish.py index 43d3b029dc..dd65030f7a 100644 --- a/pype/modules/standalonepublish/publish.py +++ b/pype/modules/standalonepublish/publish.py @@ -147,7 +147,7 @@ def cli_publish(data, gui=True): envcopy["PYBLISH_HOSTS"] = "standalonepublisher" envcopy["SAPUBLISH_INPATH"] = json_data_path envcopy["SAPUBLISH_OUTPATH"] = return_data_path - envcopy["PYBLISH_GUI"] = "pyblish_lite" + envcopy["PYBLISH_GUI"] = "pyblish_pype" returncode = execute([ sys.executable, "-u", "-m", "pyblish" From 9a7412d964c571be47443ac6b44c622482e4b42d Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 11 Jun 2020 23:37:14 +0100 Subject: [PATCH 046/131] Ensure scene settings on application launch. --- pype/hosts/harmony/__init__.py | 42 ++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index 765dcb9e72..22d70b179d 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -1,7 +1,46 @@ import os +import time from avalon import api, harmony import pyblish.api +from pype import lib + + +def ensure_scene_settings(): + fps = lib.get_asset()["data"]["fps"] + frame_start = lib.get_asset()["data"]["frameStart"] + frame_end = lib.get_asset()["data"]["frameEnd"] + + settings = { + "setFrameRate": fps, + "setStartFrame": frame_start, + "setStopFrame": frame_end + } + func = """function func(arg) + {{ + scene.{method}(arg); + }} + func + """ + for key, value in settings.items(): + if value is None: + continue + + # Need to wait to not spam Harmony with multiple requests at the same + # time. + time.sleep(1) + + harmony.send({"function": func.format(method=key), "args": [value]}) + + time.sleep(1) + + func = """function func(arg) + { + frame.remove(arg, frame.numberOf() - arg); + } + func + """ + harmony.send({"function": func, "args": [frame_end]}) def install(): @@ -23,10 +62,13 @@ def install(): api.Creator, os.path.join(plugins_directory, "create") ) + # Register callbacks. pyblish.api.register_callback( "instanceToggled", on_pyblish_instance_toggled ) + api.on("application.launched", ensure_scene_settings) + def on_pyblish_instance_toggled(instance, old_value, new_value): """Toggle node enabling on instance toggles.""" From 59d3ab14b2713c6853c85960f7731ac3f7ea829f Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 11 Jun 2020 23:38:55 +0100 Subject: [PATCH 047/131] Publish workfile --- .../harmony/publish/collect_workfile.py | 28 +++++++++++++ .../harmony/publish/extract_save_scene.py | 13 ++++++ .../harmony/publish/extract_workfile.py | 34 ++++++++++++++++ .../harmony/publish/increment_workfile.py | 40 +++++++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 pype/plugins/harmony/publish/collect_workfile.py create mode 100644 pype/plugins/harmony/publish/extract_save_scene.py create mode 100644 pype/plugins/harmony/publish/extract_workfile.py create mode 100644 pype/plugins/harmony/publish/increment_workfile.py diff --git a/pype/plugins/harmony/publish/collect_workfile.py b/pype/plugins/harmony/publish/collect_workfile.py new file mode 100644 index 0000000000..7781eb0774 --- /dev/null +++ b/pype/plugins/harmony/publish/collect_workfile.py @@ -0,0 +1,28 @@ +import pyblish.api +import os + + +class CollectWorkfile(pyblish.api.ContextPlugin): + """Collect current script for publish.""" + + order = pyblish.api.CollectorOrder + 0.1 + label = "Collect Workfile" + hosts = ["harmony"] + + def process(self, context): + family = "workfile" + task = os.getenv("AVALON_TASK", None) + subset = family + task.capitalize() + basename = os.path.basename(context.data["currentFile"]) + + # Create instance + instance = context.create_instance(subset) + instance.data.update({ + "subset": subset, + "label": basename, + "name": basename, + "family": family, + "families": [], + "representations": [], + "asset": os.environ["AVALON_ASSET"] + }) diff --git a/pype/plugins/harmony/publish/extract_save_scene.py b/pype/plugins/harmony/publish/extract_save_scene.py new file mode 100644 index 0000000000..1733bdb95c --- /dev/null +++ b/pype/plugins/harmony/publish/extract_save_scene.py @@ -0,0 +1,13 @@ +import pyblish.api +from avalon import harmony + + +class ExtractSaveScene(pyblish.api.ContextPlugin): + """Save scene for extraction.""" + + label = "Extract Save Scene" + order = pyblish.api.ExtractorOrder - 0.49 + hosts = ["harmony"] + + def process(self, instance): + harmony.save_scene() diff --git a/pype/plugins/harmony/publish/extract_workfile.py b/pype/plugins/harmony/publish/extract_workfile.py new file mode 100644 index 0000000000..3c51c94452 --- /dev/null +++ b/pype/plugins/harmony/publish/extract_workfile.py @@ -0,0 +1,34 @@ +import os +import shutil + +import pype.api +from avalon import harmony + + +class ExtractWorkfile(pype.api.Extractor): + """Extract the connected nodes to the composite instance.""" + + label = "Extract Workfile" + hosts = ["harmony"] + families = ["workfile"] + + def process(self, instance): + file_path = instance.context.data["currentFile"] + staging_dir = self.staging_dir(instance) + + os.chdir(staging_dir) + shutil.make_archive( + instance.name, + "zip", + os.path.dirname(file_path) + ) + zip_path = os.path.join(staging_dir, instance.name + ".zip") + self.log.info(f"Output zip file: {zip_path}") + + representation = { + "name": "tpl", + "ext": "zip", + "files": "{}.zip".format(instance.name), + "stagingDir": staging_dir + } + instance.data["representations"] = [representation] diff --git a/pype/plugins/harmony/publish/increment_workfile.py b/pype/plugins/harmony/publish/increment_workfile.py new file mode 100644 index 0000000000..0b626ed190 --- /dev/null +++ b/pype/plugins/harmony/publish/increment_workfile.py @@ -0,0 +1,40 @@ +import os +import shutil + +import pyblish.api +from pype.action import get_errored_plugins_from_data +from pype.lib import version_up +import pype.api +from avalon import harmony + + +class IncrementWorkfile(pyblish.api.InstancePlugin): + """Increment the current workfile. + + Saves the current scene with an increased version number. + """ + + label = "Increment Workfile" + order = pyblish.api.IntegratorOrder + 9.0 + hosts = ["harmony"] + families = ["workfile"] + optional = True + + def process(self, instance): + errored_plugins = get_errored_plugins_from_data(instance.context) + if errored_plugins: + raise RuntimeError( + "Skipping incrementing current file because submission to" + " deadline failed." + ) + + scene_dir = version_up( + os.path.dirname(instance.context.data["currentFile"]) + ) + scene_path = os.path.join( + scene_dir, os.path.basename(scene_dir) + ".xstage" + ) + + harmony.save_scene_as(scene_path) + + self.log.info("Incremented workfile to: {}".format(scene_path)) From 369e35c6ef17f60c1b9e380c4b19c2646a641d9e Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 11 Jun 2020 23:39:53 +0100 Subject: [PATCH 048/131] Loader for templates and workfiles --- .../harmony/load/load_template_workfile.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 pype/plugins/harmony/load/load_template_workfile.py diff --git a/pype/plugins/harmony/load/load_template_workfile.py b/pype/plugins/harmony/load/load_template_workfile.py new file mode 100644 index 0000000000..00d2e63c62 --- /dev/null +++ b/pype/plugins/harmony/load/load_template_workfile.py @@ -0,0 +1,69 @@ +import tempfile +import zipfile +import os +import shutil + +from avalon import api, harmony + + +class ImportTemplateLoader(api.Loader): + """Import templates.""" + + families = ["harmony.template"] + representations = ["*"] + label = "Import Template" + + def load(self, context, name=None, namespace=None, data=None): + # Make backdrops from metadata. + backdrops = context["representation"]["data"].get("backdrops", []) + + func = """function func(args) + { + Backdrop.addBackdrop("Top", args[0]); + } + func + """ + for backdrop in backdrops: + harmony.send({"function": func, "args": [backdrop]}) + + # Import template. + temp_dir = tempfile.mkdtemp() + zip_file = api.get_representation_path(context["representation"]) + template_path = os.path.join(temp_dir, "temp.tpl") + with zipfile.ZipFile(zip_file, "r") as zip_ref: + zip_ref.extractall(template_path) + + func = """function func(args) + { + var template_path = args[0]; + var drag_object = copyPaste.copyFromTemplate( + template_path, 0, 0, copyPaste.getCurrentCreateOptions() + ); + copyPaste.pasteNewNodes( + drag_object, "", copyPaste.getCurrentPasteOptions() + ); + } + func + """ + + func = """function func(args) + { + var template_path = args[0]; + var drag_object = copyPaste.pasteTemplateIntoGroup( + template_path, "Top", 1 + ); + } + func + """ + + harmony.send({"function": func, "args": [template_path]}) + + shutil.rmtree(temp_dir) + + +class ImportWorkfileLoader(ImportTemplateLoader): + """Import workfiles.""" + + families = ["workfile"] + representations = ["*"] + label = "Import Workfile" From 01bef1f2a977b8426d464ccb24bf702f29209e02 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 11 Jun 2020 23:41:25 +0100 Subject: [PATCH 049/131] Fix extract render - Harmony always returns error code. Ignoring return code. - Fix missing thumbnail. --- .../plugins/harmony/publish/extract_render.py | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/pype/plugins/harmony/publish/extract_render.py b/pype/plugins/harmony/publish/extract_render.py index de6e8b9008..f02ed66e5f 100644 --- a/pype/plugins/harmony/publish/extract_render.py +++ b/pype/plugins/harmony/publish/extract_render.py @@ -1,9 +1,9 @@ import os import tempfile +import subprocess import pyblish.api from avalon import harmony -import pype.lib import clique @@ -37,7 +37,6 @@ class ExtractRender(pyblish.api.InstancePlugin): {"function": func, "args": [instance[0]]} )["result"] application_path = result[0] - project_path = result[1] scene_path = os.path.join(result[1], result[2] + ".xstage") frame_rate = result[3] frame_start = result[4] @@ -59,9 +58,16 @@ class ExtractRender(pyblish.api.InstancePlugin): ) harmony.save_scene() - # Execute rendering. - output = pype.lib._subprocess([application_path, "-batch", scene_path]) - self.log.info(output) + # Execute rendering. Ignoring error cause Harmony returns error code + # always. + proc = subprocess.Popen( + [application_path, "-batch", scene_path], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.PIPE + ) + output, error = proc.communicate() + self.log.info(output.decode("utf-8")) # Collect rendered files. files = os.listdir(path) @@ -77,6 +83,30 @@ class ExtractRender(pyblish.api.InstancePlugin): ) ) + # Generate thumbnail. + thumbnail_path = os.path.join(path, "thumbnail.png") + args = [ + "ffmpeg", "-y", + "-i", os.path.join(path, list(collections[0])[0]), + "-vf", "scale=300:-1", + "-vframes", "1", + thumbnail_path + ] + process = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.PIPE + ) + + output = process.communicate()[0] + + if process.returncode != 0: + raise ValueError(output.decode("utf-8")) + + self.log.debug(output.decode("utf-8")) + + # Generate representations. extension = os.path.splitext(list(collections[0])[0])[-1][1:] representation = { "name": extension, @@ -89,12 +119,18 @@ class ExtractRender(pyblish.api.InstancePlugin): "preview": True, "tags": ["review"] } - instance.data["representations"] = [representation] - self.log.info(frame_rate) + thumbnail = { + "name": "thumbnail", + "ext": "png", + "files": os.path.basename(thumbnail_path), + "stagingDir": path, + "tags": ["thumbnail"] + } + instance.data["representations"] = [representation, thumbnail] # Required for extract_review plugin (L222 onwards). instance.data["frameStart"] = frame_start instance.data["frameEnd"] = frame_end instance.data["fps"] = frame_rate - self.log.info("Extracted {instance} to {path}".format(**locals())) + self.log.info(f"Extracted {instance} to {path}") From 918ec2c6ec44757e2145c9354beff2ec930239e6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 11 Jun 2020 23:49:47 +0100 Subject: [PATCH 050/131] Hound --- pype/plugins/harmony/publish/extract_workfile.py | 1 - pype/plugins/harmony/publish/increment_workfile.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/pype/plugins/harmony/publish/extract_workfile.py b/pype/plugins/harmony/publish/extract_workfile.py index 3c51c94452..7a0a7954dd 100644 --- a/pype/plugins/harmony/publish/extract_workfile.py +++ b/pype/plugins/harmony/publish/extract_workfile.py @@ -2,7 +2,6 @@ import os import shutil import pype.api -from avalon import harmony class ExtractWorkfile(pype.api.Extractor): diff --git a/pype/plugins/harmony/publish/increment_workfile.py b/pype/plugins/harmony/publish/increment_workfile.py index 0b626ed190..29bae09df3 100644 --- a/pype/plugins/harmony/publish/increment_workfile.py +++ b/pype/plugins/harmony/publish/increment_workfile.py @@ -1,10 +1,8 @@ import os -import shutil import pyblish.api from pype.action import get_errored_plugins_from_data from pype.lib import version_up -import pype.api from avalon import harmony From a016a00c37e9a5b8830d9d6a6802eb369b6ec0c0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 12 Jun 2020 10:09:57 +0200 Subject: [PATCH 051/131] fixed config imports --- pype/tools/pyblish_pype/control.py | 8 +++----- pype/tools/pyblish_pype/model.py | 9 +++------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/pype/tools/pyblish_pype/control.py b/pype/tools/pyblish_pype/control.py index e64f3d5bfb..4796708442 100644 --- a/pype/tools/pyblish_pype/control.py +++ b/pype/tools/pyblish_pype/control.py @@ -19,10 +19,8 @@ import pyblish.version from . import util from .constants import InstanceStates -try: - from pypeapp.config import get_presets -except Exception: - get_presets = dict + +from pype.api import config class IterationBreak(Exception): @@ -114,7 +112,7 @@ class Controller(QtCore.QObject): def presets_by_hosts(self): # Get global filters as base - presets = get_presets().get("plugins", {}) + presets = config.get_presets().get("plugins", {}) if not presets: return {} diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index 58ab3ed0b7..26f0189730 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -36,11 +36,8 @@ from .vendor.six.moves import queue from .vendor import qtawesome from .constants import PluginStates, InstanceStates, GroupStates, Roles -try: - from pypeapp import config - get_presets = config.get_presets -except Exception: - get_presets = dict +from pype.api import config + # ItemTypes InstanceType = QtGui.QStandardItem.UserType @@ -106,7 +103,7 @@ class IntentModel(QtGui.QStandardItemModel): self.default_index = 0 intents_preset = ( - get_presets() + config.get_presets() .get("tools", {}) .get("pyblish", {}) .get("ui", {}) From e99ab29c6c43f7ada8dd865888c92e74c50e6c45 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 Jun 2020 12:17:06 +0300 Subject: [PATCH 052/131] fix(celaction): PR suggested changes --- pype/lib.py | 13 ++++++++----- pype/plugins/celaction/publish/collect_audio.py | 2 +- .../publish/collect_celaction_instances.py | 5 +---- pype/plugins/global/publish/submit_publish_job.py | 7 +------ 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index 092079ccb3..7c7a01d5cc 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -469,7 +469,7 @@ def get_version_from_path(file): ) -def get_last_version_from_path(path_dir, filter=None): +def get_last_version_from_path(path_dir, filter): """ Finds last version of given directory content @@ -480,16 +480,19 @@ def get_last_version_from_path(path_dir, filter=None): Returns: string: file name with last version + Example: + last_version_file = get_last_version_from_path( + "/project/shots/shot01/work", ["shot01", "compositing", "nk"]) """ + assert os.path.isdir(path_dir), "`path_dir` argument needs to be directory" + assert isinstance(filter, list) and ( + len(filter) != 0), "`filter` argument needs to be list and not empty" filtred_files = list() # form regex for filtering - patern = r".*" - - if filter: - patern = patern.join(filter) + patern = r".*".join(filter) for f in os.listdir(path_dir): if not re.findall(patern, f): diff --git a/pype/plugins/celaction/publish/collect_audio.py b/pype/plugins/celaction/publish/collect_audio.py index 89fe6bf966..610b81d056 100644 --- a/pype/plugins/celaction/publish/collect_audio.py +++ b/pype/plugins/celaction/publish/collect_audio.py @@ -4,6 +4,7 @@ import os import pype.api as pype from pprint import pformat + class AppendCelactionAudio(pyblish.api.ContextPlugin): label = "Colect Audio for publishing" @@ -12,7 +13,6 @@ class AppendCelactionAudio(pyblish.api.ContextPlugin): def process(self, context): self.log.info('Collecting Audio Data') asset_entity = context.data["assetEntity"] - asset_id = asset_entity["_id"] # get all available representations subsets = pype.get_subsets(asset_entity["name"], diff --git a/pype/plugins/celaction/publish/collect_celaction_instances.py b/pype/plugins/celaction/publish/collect_celaction_instances.py index 583cb7514b..aa2bb5da5d 100644 --- a/pype/plugins/celaction/publish/collect_celaction_instances.py +++ b/pype/plugins/celaction/publish/collect_celaction_instances.py @@ -1,7 +1,7 @@ import os from avalon import api import pyblish.api -from pype import api as pype + class CollectCelactionInstances(pyblish.api.ContextPlugin): """ Adds the celaction render instances """ @@ -36,7 +36,6 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin): if celaction_kwargs: shared_instance_data.update(celaction_kwargs) - # ___________________________________________ # workfile instance family = "workfile" subset = family + task.capitalize() @@ -67,7 +66,6 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin): self.log.info('Publishing Celaction workfile') - # ___________________________________________ # render instance family = "render.farm" subset = f"render{task}Main" @@ -89,6 +87,5 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin): self.log.info('Publishing Celaction render instance') self.log.debug(f"Instance data: `{instance.data}`") - for i in context: self.log.debug(f"{i.data['families']}") diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 6da459690d..64adf74fe8 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -214,10 +214,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): ).format(output_dir)) rootless_path = output_dir - # gets script path - script_path = _get_script() - self.log.info("Adding script path: `{}`...".format(script_path)) - # Generate the payload for Deadline submission payload = { "JobInfo": { @@ -240,7 +236,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): }, "PluginInfo": { "Version": "3.6", - "ScriptFile": script_path, + "ScriptFile": _get_script(), "Arguments": "", "SingleFrameOnly": "True", }, @@ -254,7 +250,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): metadata_path = os.path.join(rootless_path, metadata_filename) environment = job["Props"].get("Env", {}) - environment["PYPE_LOG_NO_COLORS"] = "1" environment["PYPE_METADATA_FILE"] = metadata_path environment["AVALON_PROJECT"] = io.Session["AVALON_PROJECT"] environment["PYPE_LOG_NO_COLORS"] = "1" From 81219ff4b7a1299a7ab9318d3d082d5efe629bcb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 Jun 2020 12:39:05 +0300 Subject: [PATCH 053/131] fix(celaction): Hound suggestion --- pype/plugins/celaction/publish/collect_render_path.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/plugins/celaction/publish/collect_render_path.py b/pype/plugins/celaction/publish/collect_render_path.py index 3d092ccf22..cddd2643d8 100644 --- a/pype/plugins/celaction/publish/collect_render_path.py +++ b/pype/plugins/celaction/publish/collect_render_path.py @@ -2,7 +2,6 @@ import os import pyblish.api - class CollectRenderPath(pyblish.api.InstancePlugin): """Generate file and directory path where rendered images will be""" From f5b02d3374c7ff12d0ce4a7a65c558b3ba010e75 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 12 Jun 2020 11:24:41 +0100 Subject: [PATCH 054/131] Improve ensuring scene setting - only one call to Harmony - message box for missing attributes - resolution attributes. --- pype/hosts/harmony/__init__.py | 75 ++++++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index 22d70b179d..df9485b4c7 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -1,46 +1,79 @@ import os import time +import sys from avalon import api, harmony +from avalon.vendor import Qt import pyblish.api from pype import lib def ensure_scene_settings(): - fps = lib.get_asset()["data"]["fps"] - frame_start = lib.get_asset()["data"]["frameStart"] - frame_end = lib.get_asset()["data"]["frameEnd"] + asset_data = lib.get_asset()["data"] + fps = asset_data["fps"] + frame_start = asset_data["frameStart"] + frame_end = asset_data["frameEnd"] + resolution_width = asset_data.get("resolutionWidth") + resolution_height = asset_data.get("resolutionHeight") settings = { - "setFrameRate": fps, - "setStartFrame": frame_start, - "setStopFrame": frame_end + "fps": fps, + "frameStart": frame_start, + "frameEnd": frame_end, + "resolutionWidth": resolution_width, + "resolutionHeight": resolution_height } - func = """function func(arg) - {{ - scene.{method}(arg); - }} - func - """ + + invalid_settings = [] + valid_settings = {} for key, value in settings.items(): if value is None: - continue + invalid_settings.append(key) + else: + valid_settings[key] = value - # Need to wait to not spam Harmony with multiple requests at the same - # time. - time.sleep(1) + # Warn about missing attributes. + print("Starting new QApplication..") + app = Qt.QtWidgets.QApplication(sys.argv) - harmony.send({"function": func.format(method=key), "args": [value]}) + message_box = Qt.QtWidgets.QMessageBox() + message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning) + msg = "Missing attributes:" + if invalid_settings: + for item in invalid_settings: + msg += f"\n{item}" + message_box.setText(msg) + message_box.exec_() - time.sleep(1) + # Garbage collect QApplication. + del app - func = """function func(arg) + func = """function func(args) { - frame.remove(arg, frame.numberOf() - arg); + if (args["fps"]) + { + scene.setFrameRate(); + } + if (args["frameStart"]) + { + scene.setStartFrame(args[1]); + } + if (args["frameEnd"]) + { + scene.setStopFrame(args[2]); + frame.remove(args[2], frame.numberOf() - args[2]); + } + if (args["resolutionWidth"] && args["resolutionHeight"]) + { + scene.setDefaultResolution( + args["resolutionWidth"], args["resolutionHeight"], 41.112 + ) + } } func """ - harmony.send({"function": func, "args": [frame_end]}) + + harmony.send({"function": func, "args": [valid_settings]}) def install(): From a0cce6522976095a6f22c968c5700548ed444128 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 12 Jun 2020 11:25:13 +0100 Subject: [PATCH 055/131] Hound --- pype/hosts/harmony/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index df9485b4c7..48967b0a0f 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -1,5 +1,4 @@ import os -import time import sys from avalon import api, harmony From a59c2225d53541a835a6883cac35a81680b5f9b4 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 12 Jun 2020 11:46:32 +0100 Subject: [PATCH 056/131] Fix harmony request. --- pype/hosts/harmony/__init__.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index 48967b0a0f..6cc5d50eb6 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -49,29 +49,30 @@ def ensure_scene_settings(): func = """function func(args) { - if (args["fps"]) + if (args[0]["fps"]) { - scene.setFrameRate(); + scene.setFrameRate(args[0]["fps"]); } - if (args["frameStart"]) + if (args[0]["frameStart"]) { - scene.setStartFrame(args[1]); + scene.setStartFrame(args[0]["frameStart"]); } - if (args["frameEnd"]) + if (args[0]["frameEnd"]) { - scene.setStopFrame(args[2]); - frame.remove(args[2], frame.numberOf() - args[2]); + scene.setStopFrame(args[0]["frameEnd"]); + frame.remove( + args[0]["frameEnd"], frame.numberOf() - args[0]["frameEnd"] + ); } - if (args["resolutionWidth"] && args["resolutionHeight"]) + if (args[0]["resolutionWidth"] && args[0]["resolutionHeight"]) { scene.setDefaultResolution( - args["resolutionWidth"], args["resolutionHeight"], 41.112 + args[0]["resolutionWidth"], args[0]["resolutionHeight"], 41.112 ) } } func """ - harmony.send({"function": func, "args": [valid_settings]}) From 090ceae3dddbe1b085dbe4d343ae2b40ff431c12 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 12 Jun 2020 11:54:52 +0100 Subject: [PATCH 057/131] Validate instances - asset only atm --- .../harmony/publish/validate_instances.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 pype/plugins/harmony/publish/validate_instances.py diff --git a/pype/plugins/harmony/publish/validate_instances.py b/pype/plugins/harmony/publish/validate_instances.py new file mode 100644 index 0000000000..f084baf790 --- /dev/null +++ b/pype/plugins/harmony/publish/validate_instances.py @@ -0,0 +1,48 @@ +import os + +import pyblish.api +import pype.api +from avalon import harmony + + +class ValidateInstanceRepair(pyblish.api.Action): + """Repair the instance.""" + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + + # Get the errored instances + failed = [] + for result in context.data["results"]: + if (result["error"] is not None and result["instance"] is not None + and result["instance"] not in failed): + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + + for instance in instances: + data = harmony.read(instance[0]) + data["asset"] = os.environ["AVALON_ASSET"] + harmony.imprint(instance[0], data) + + +class ValidateInstance(pyblish.api.InstancePlugin): + """Validate the instance asset is the current asset.""" + + label = "Validate Instance" + hosts = ["harmony"] + actions = [ValidateInstanceRepair] + order = pype.api.ValidateContentsOrder + + def process(self, instance): + instance_asset = instance.data["asset"] + current_asset = os.environ["AVALON_ASSET"] + msg = ( + "Instance asset is not the same as current asset:" + f"\nInstance: {instance_asset}\nCurrent: {current_asset}" + ) + assert instance_asset == current_asset, msg From 978e24d59b3209acf760ef0146e856ef8df63d9f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Jun 2020 14:02:00 +0200 Subject: [PATCH 058/131] fixing subset attaching assert, disabling arnold scene export --- pype/plugins/maya/create/create_render.py | 2 +- pype/plugins/maya/publish/collect_render.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index b2256af455..4d775dab52 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -35,7 +35,7 @@ class CreateRender(avalon.maya.Creator): _image_prefixes = { 'mentalray': 'maya///_', - 'vray': '"maya///', + 'vray': 'maya///', 'arnold': 'maya///_', 'renderman': 'maya///_', 'redshift': 'maya///_' diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index bfb6e79cba..41b4f2cc7c 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -192,7 +192,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # in expectedFiles. If so, raise error as we cannot attach AOV # (considered to be subset on its own) to another subset if attachTo: - assert len(exp_files[0].keys()) == 1, ( + assert isinstance(exp_files, dict) is True, ( "attaching multiple AOVs or renderable cameras to " "subset is not supported" ) From 587091f3accfe64bd37329aedb2bfa836bf91e29 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Jun 2020 14:06:55 +0200 Subject: [PATCH 059/131] fix float start and end frames --- pype/plugins/maya/publish/extract_animation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pype/plugins/maya/publish/extract_animation.py b/pype/plugins/maya/publish/extract_animation.py index bd57eca0f8..f7058b34f1 100644 --- a/pype/plugins/maya/publish/extract_animation.py +++ b/pype/plugins/maya/publish/extract_animation.py @@ -37,7 +37,7 @@ class ExtractAnimation(pype.api.Extractor): # Collect the start and end including handles start = instance.data["frameStart"] end = instance.data["frameEnd"] - handles = instance.data.get("handles", 0) + handles = instance.data.get("handles", 0) or 0 if handles: start -= handles end += handles @@ -50,7 +50,7 @@ class ExtractAnimation(pype.api.Extractor): path = os.path.join(parent_dir, filename) options = { - "step": instance.data.get("step", 1.0), + "step": instance.data.get("step", 1.0) or 1.0, "attr": ["cbId"], "writeVisibility": True, "writeCreases": True, @@ -74,8 +74,8 @@ class ExtractAnimation(pype.api.Extractor): with avalon.maya.maintained_selection(): cmds.select(nodes, noExpand=True) extract_alembic(file=path, - startFrame=start, - endFrame=end, + startFrame=float(start), + endFrame=float(end), **options) if "representations" not in instance.data: From df063665c4023618892a3d4fca8bf0163950a1a6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Jun 2020 18:04:00 +0200 Subject: [PATCH 060/131] fix type check and enable arnold specific validator only when in arnold --- pype/plugins/maya/publish/collect_render.py | 2 +- .../maya/publish/validate_mesh_arnold_attributes.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 41b4f2cc7c..ca5c403808 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -192,7 +192,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # in expectedFiles. If so, raise error as we cannot attach AOV # (considered to be subset on its own) to another subset if attachTo: - assert isinstance(exp_files, dict) is True, ( + assert isinstance(exp_files, list), ( "attaching multiple AOVs or renderable cameras to " "subset is not supported" ) diff --git a/pype/plugins/maya/publish/validate_mesh_arnold_attributes.py b/pype/plugins/maya/publish/validate_mesh_arnold_attributes.py index 1ddf0285d4..04a3cf3c79 100644 --- a/pype/plugins/maya/publish/validate_mesh_arnold_attributes.py +++ b/pype/plugins/maya/publish/validate_mesh_arnold_attributes.py @@ -1,5 +1,5 @@ import pymel.core as pc - +from maya import cmds import pyblish.api import pype.api import pype.hosts.maya.action @@ -23,6 +23,11 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin): pype.api.RepairAction ] optional = True + if cmds.getAttr( + "defaultRenderGlobals.currentRenderer").lower() == "arnold": + active = True + else: + active = False @classmethod def get_invalid_attributes(cls, instance, compute=False): From d97447bd578ef83a24ff998f76060e4970c7ffd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 12 Jun 2020 22:16:47 +0200 Subject: [PATCH 061/131] fix get_invalid signature --- pype/plugins/maya/publish/validate_transform_naming_suffix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/maya/publish/validate_transform_naming_suffix.py b/pype/plugins/maya/publish/validate_transform_naming_suffix.py index bd7db437fa..17c434d9af 100644 --- a/pype/plugins/maya/publish/validate_transform_naming_suffix.py +++ b/pype/plugins/maya/publish/validate_transform_naming_suffix.py @@ -63,7 +63,7 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin): return False @classmethod - def get_invalid(cls, instance, SUFFIX_NAMING_TABLE, ALLOW_IF_NOT_IN_SUFFIX_TABLE): + def get_invalid(cls, instance): transforms = cmds.ls(instance, type='transform', long=True) invalid = [] @@ -74,7 +74,7 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin): noIntermediate=True) shape_type = cmds.nodeType(shapes[0]) if shapes else None - if not cls.is_valid_name(transform, shape_type, SUFFIX_NAMING_TABLE, ALLOW_IF_NOT_IN_SUFFIX_TABLE): + if not cls.is_valid_name(transform, shape_type, cls.SUFFIX_NAMING_TABLE, cls.ALLOW_IF_NOT_IN_SUFFIX_TABLE): invalid.append(transform) return invalid From 9390e6e101fc4a807595a8dd6cab40a3b7730d61 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 12 Jun 2020 22:29:22 +0200 Subject: [PATCH 062/131] shutup hound and add docstrings --- .../validate_transform_naming_suffix.py | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/pype/plugins/maya/publish/validate_transform_naming_suffix.py b/pype/plugins/maya/publish/validate_transform_naming_suffix.py index 17c434d9af..17066f6b12 100644 --- a/pype/plugins/maya/publish/validate_transform_naming_suffix.py +++ b/pype/plugins/maya/publish/validate_transform_naming_suffix.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Plugin for validating naming conventions.""" from maya import cmds import pyblish.api @@ -42,7 +44,8 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin): ALLOW_IF_NOT_IN_SUFFIX_TABLE = True @staticmethod - def is_valid_name(node_name, shape_type, SUFFIX_NAMING_TABLE, ALLOW_IF_NOT_IN_SUFFIX_TABLE): + def is_valid_name(node_name, shape_type, + SUFFIX_NAMING_TABLE, ALLOW_IF_NOT_IN_SUFFIX_TABLE): """Return whether node's name is correct. The correctness for a transform's suffix is dependent on what @@ -52,6 +55,12 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin): When `shape_type` is None the transform doesn't have any direct children shapes. + Args: + node_name (str): Node name. + shape_type (str): Type of node. + SUFFIX_NAMING_TABLE (dict): Mapping dict for suffixes. + ALLOW_IF_NOT_IN_SUFFIX_TABLE (dict): Filter dict. + """ if shape_type not in SUFFIX_NAMING_TABLE: return ALLOW_IF_NOT_IN_SUFFIX_TABLE @@ -64,6 +73,12 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): + """Get invalid nodes in instance. + + Args: + instance (:class:`pyblish.api.Instance`): published instance. + + """ transforms = cmds.ls(instance, type='transform', long=True) invalid = [] @@ -74,16 +89,23 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin): noIntermediate=True) shape_type = cmds.nodeType(shapes[0]) if shapes else None - if not cls.is_valid_name(transform, shape_type, cls.SUFFIX_NAMING_TABLE, cls.ALLOW_IF_NOT_IN_SUFFIX_TABLE): + if not cls.is_valid_name(transform, shape_type, + cls.SUFFIX_NAMING_TABLE, + cls.ALLOW_IF_NOT_IN_SUFFIX_TABLE): invalid.append(transform) return invalid def process(self, instance): - """Process all the nodes in the instance""" + """Process all the nodes in the instance. + Args: + instance (:class:`pyblish.api.Instance`): published instance. - invalid = self.get_invalid(instance, self.SUFFIX_NAMING_TABLE, self.ALLOW_IF_NOT_IN_SUFFIX_TABLE) + """ + invalid = self.get_invalid(instance, + self.SUFFIX_NAMING_TABLE, + self.ALLOW_IF_NOT_IN_SUFFIX_TABLE) if invalid: raise ValueError("Incorrectly named geometry " "transforms: {0}".format(invalid)) From cad378fca021e7e4db11bcd31658373090785922 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 14 Jun 2020 19:09:51 +0100 Subject: [PATCH 063/131] Extract shot mov and wav. --- .../plugins/harmony/publish/extract_render.py | 2 +- .../publish/collect_shots.py | 34 +++++---- .../publish/extract_review.py | 2 +- .../publish/extract_shot.py | 70 +++++++++++++++++++ .../publish/validate_editorial_resources.py | 28 ++++++++ 5 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 pype/plugins/standalonepublisher/publish/extract_shot.py create mode 100644 pype/plugins/standalonepublisher/publish/validate_editorial_resources.py diff --git a/pype/plugins/harmony/publish/extract_render.py b/pype/plugins/harmony/publish/extract_render.py index de6e8b9008..56e7973f3a 100644 --- a/pype/plugins/harmony/publish/extract_render.py +++ b/pype/plugins/harmony/publish/extract_render.py @@ -87,7 +87,7 @@ class ExtractRender(pyblish.api.InstancePlugin): "frameEnd": frame_end, "fps": frame_rate, "preview": True, - "tags": ["review"] + "tags": ["review", "ftrackreview"] } instance.data["representations"] = [representation] self.log.info(frame_rate) diff --git a/pype/plugins/standalonepublisher/publish/collect_shots.py b/pype/plugins/standalonepublisher/publish/collect_shots.py index fb566a15ee..be7d5691ba 100644 --- a/pype/plugins/standalonepublisher/publish/collect_shots.py +++ b/pype/plugins/standalonepublisher/publish/collect_shots.py @@ -1,5 +1,4 @@ import os -from urllib.parse import unquote, urlparse import opentimelineio as otio from bson import json_util @@ -41,7 +40,16 @@ class CollectShots(pyblish.api.InstancePlugin): file_path = os.path.join( representation["stagingDir"], representation["files"] ) - timeline = otio.adapters.read_from_file(file_path) + instance.context.data["editorialPath"] = file_path + + extension = os.path.splitext(file_path)[1][1:] + kwargs = {} + if extension == "edl": + # EDL has no frame rate embedded so needs explicit frame rate else + # 24 is asssumed. + kwargs["rate"] = lib.get_asset()["data"]["fps"] + + timeline = otio.adapters.read_from_file(file_path, **kwargs) tracks = timeline.each_child( descended_from_type=otio.schema.track.Track ) @@ -61,13 +69,6 @@ class CollectShots(pyblish.api.InstancePlugin): instances = [] for child in track.each_child(): - parse = urlparse(child.media_reference.target_url) - - # XML files from NukeStudio has extra "/" at the front of path. - path = os.path.normpath( - os.path.abspath(unquote(parse.path)[1:]) - ) - frame_start = child.range_in_parent().start_time.value frame_end = child.range_in_parent().end_time_inclusive().value @@ -77,12 +78,15 @@ class CollectShots(pyblish.api.InstancePlugin): instance.context.create_instance(**{ "name": name, "label": label, - "path": path, "frameStart": frame_start, "frameEnd": frame_end, "family": "shot", + "families": ["review", "ftrack"], + "ftrackFamily": "review", "asset": name, - "subset": "shotMain" + "subset": "shotMain", + "representations": [], + "source": file_path }) ) @@ -108,7 +112,12 @@ class CollectShots(pyblish.api.InstancePlugin): else: for instance in instances: childs[instance.data["name"]] = { - "childs": {}, "entity_type": "Shot" + "childs": {}, + "entity_type": "Shot", + "custom_attributes": { + "frameStart": instance.data["frameStart"], + "frameEnd": instance.data["frameEnd"] + } } context_hierarchy = { @@ -121,5 +130,6 @@ class CollectShots(pyblish.api.InstancePlugin): context_hierarchy = {name: context_hierarchy} instance.context.data["hierarchyContext"] = context_hierarchy self.log.info( + "Hierarchy:\n" + json_util.dumps(context_hierarchy, sort_keys=True, indent=4) ) diff --git a/pype/plugins/standalonepublisher/publish/extract_review.py b/pype/plugins/standalonepublisher/publish/extract_review.py index 36793d4c62..0f845afcb1 100644 --- a/pype/plugins/standalonepublisher/publish/extract_review.py +++ b/pype/plugins/standalonepublisher/publish/extract_review.py @@ -42,7 +42,7 @@ class ExtractReviewSP(pyblish.api.InstancePlugin): self.log.debug("Families In: `{}`".format(instance.data["families"])) # get specific profile if was defined - specific_profiles = instance.data.get("repreProfiles") + specific_profiles = instance.data.get("repreProfiles", []) new_repres = [] # filter out mov and img sequences diff --git a/pype/plugins/standalonepublisher/publish/extract_shot.py b/pype/plugins/standalonepublisher/publish/extract_shot.py new file mode 100644 index 0000000000..34d2092d77 --- /dev/null +++ b/pype/plugins/standalonepublisher/publish/extract_shot.py @@ -0,0 +1,70 @@ +import os + +import pype.api +import pype.lib + + +class ExtractShot(pype.api.Extractor): + """Extract shot "mov" and "wav" files.""" + + label = "Extract Shot" + hosts = ["standalonepublisher"] + families = ["shot"] + + def process(self, instance): + staging_dir = self.staging_dir(instance) + self.log.info("Outputting shot to {}".format(staging_dir)) + + editorial_path = instance.context.data["editorialPath"] + basename = os.path.splitext(os.path.basename(editorial_path))[0] + + # Generate mov file. + fps = pype.lib.get_asset()["data"]["fps"] + input_path = os.path.join( + os.path.dirname(editorial_path), basename + ".mov" + ) + shot_mov = os.path.join(staging_dir, instance.data["name"] + ".mov") + args = [ + "ffmpeg", + "-ss", str(instance.data["frameStart"] / fps), + "-i", input_path, + "-t", str( + (instance.data["frameEnd"] - instance.data["frameStart"] + 1) / + fps + ), + "-crf", "18", + "-pix_fmt", "yuv420p", + shot_mov + ] + self.log.info(f"Processing: {args}") + output = pype.lib._subprocess(args) + self.log.info(output) + + instance.data["representations"].append({ + "name": "mov", + "ext": "mov", + "files": os.path.basename(shot_mov), + "stagingDir": staging_dir, + "frameStart": instance.data["frameStart"], + "frameEnd": instance.data["frameEnd"], + "fps": fps, + "thumbnail": True, + "tags": ["review", "ftrackreview"] + }) + + # Generate wav file. + shot_wav = os.path.join(staging_dir, instance.data["name"] + ".wav") + args = ["ffmpeg", "-i", shot_mov, shot_wav] + self.log.info(f"Processing: {args}") + output = pype.lib._subprocess(args) + self.log.info(output) + + instance.data["representations"].append({ + "name": "wav", + "ext": "wav", + "files": os.path.basename(shot_wav), + "stagingDir": staging_dir + }) + + # Required for extract_review plugin (L222 onwards). + instance.data["fps"] = fps diff --git a/pype/plugins/standalonepublisher/publish/validate_editorial_resources.py b/pype/plugins/standalonepublisher/publish/validate_editorial_resources.py new file mode 100644 index 0000000000..961641b8fa --- /dev/null +++ b/pype/plugins/standalonepublisher/publish/validate_editorial_resources.py @@ -0,0 +1,28 @@ +import os + +import pyblish.api +import pype.api + + +class ValidateEditorialResources(pyblish.api.InstancePlugin): + """Validate there is a "mov" next to the editorial file.""" + + label = "Validate Editorial Resources" + hosts = ["standalonepublisher"] + families = ["editorial"] + order = pype.api.ValidateContentsOrder + + def process(self, instance): + representation = instance.data["representations"][0] + staging_dir = representation["stagingDir"] + basename = os.path.splitext( + os.path.basename(representation["files"]) + )[0] + + files = [x for x in os.listdir(staging_dir)] + + # Check for "mov" file. + filename = basename + ".mov" + filepath = os.path.join(staging_dir, filename) + msg = f"Missing \"{filepath}\"." + assert filename in files, msg From 6297607ff71c60f4ef6c50e3edbab6d388107a00 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 Jun 2020 09:36:07 +0100 Subject: [PATCH 064/131] Extract shot jpegs. --- .../publish/extract_shot.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pype/plugins/standalonepublisher/publish/extract_shot.py b/pype/plugins/standalonepublisher/publish/extract_shot.py index 34d2092d77..f2fc2b74cf 100644 --- a/pype/plugins/standalonepublisher/publish/extract_shot.py +++ b/pype/plugins/standalonepublisher/publish/extract_shot.py @@ -1,5 +1,7 @@ import os +import clique + import pype.api import pype.lib @@ -52,6 +54,29 @@ class ExtractShot(pype.api.Extractor): "tags": ["review", "ftrackreview"] }) + # Generate jpegs. + shot_jpegs = os.path.join( + staging_dir, instance.data["name"] + ".%04d.jpeg" + ) + args = ["ffmpeg", "-i", shot_mov, shot_jpegs] + self.log.info(f"Processing: {args}") + output = pype.lib._subprocess(args) + self.log.info(output) + + collection = clique.Collection( + head=instance.data["name"] + ".", tail='.jpeg', padding=4 + ) + for f in os.listdir(staging_dir): + if collection.match(f): + collection.add(f) + + instance.data["representations"].append({ + "name": "jpeg", + "ext": "jpeg", + "files": list(collection), + "stagingDir": staging_dir + }) + # Generate wav file. shot_wav = os.path.join(staging_dir, instance.data["name"] + ".wav") args = ["ffmpeg", "-i", shot_mov, shot_wav] From 42a50961473f66d0dd6498cc02a84d8a76352d65 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Jun 2020 11:34:09 +0200 Subject: [PATCH 065/131] fixed missing records on repair action processing --- pype/tools/pyblish_pype/window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 84003a88aa..a7963478d5 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -913,6 +913,7 @@ class Window(QtWidgets.QDialog): plugin_item = self.plugin_model.plugin_items[result["plugin"].id] action_state = plugin_item.data(Roles.PluginActionProgressRole) action_state |= PluginActionStates.HasFinished + result["records"] = self.terminal_model.prepare_records(result) error = result.get("error") if error: From 35e6e90db4452ca5a042311ec57de621dd1a16dc Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 Jun 2020 11:25:28 +0100 Subject: [PATCH 066/131] Ignore transitions. --- pype/plugins/standalonepublisher/publish/collect_shots.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/plugins/standalonepublisher/publish/collect_shots.py b/pype/plugins/standalonepublisher/publish/collect_shots.py index be7d5691ba..80ee875add 100644 --- a/pype/plugins/standalonepublisher/publish/collect_shots.py +++ b/pype/plugins/standalonepublisher/publish/collect_shots.py @@ -69,6 +69,12 @@ class CollectShots(pyblish.api.InstancePlugin): instances = [] for child in track.each_child(): + + # Transitions are ignored, because Clips have the full frame + # range. + if isinstance(child, otio.schema.transition.Transition): + continue + frame_start = child.range_in_parent().start_time.value frame_end = child.range_in_parent().end_time_inclusive().value From b63e6e91fc446e7402dd8228fd93195c05c70bde Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 Jun 2020 11:26:42 +0100 Subject: [PATCH 067/131] Ignore standalone host. --- .../publish/validate_custom_ftrack_attributes.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pype/plugins/global/publish/validate_custom_ftrack_attributes.py b/pype/plugins/global/publish/validate_custom_ftrack_attributes.py index 1e8b239b33..633deaa6d1 100644 --- a/pype/plugins/global/publish/validate_custom_ftrack_attributes.py +++ b/pype/plugins/global/publish/validate_custom_ftrack_attributes.py @@ -37,6 +37,21 @@ class ValidateFtrackAttributes(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder families = ["ftrack"] optional = True + # Ignore standalone host, because it does not have an Ftrack entity + # associated. + hosts = [ + "blender", + "fusion", + "harmony", + "houdini", + "maya", + "nuke", + "nukestudio", + "photoshop", + "premiere", + "resolve", + "unreal" + ] def process(self, instance): context = instance.context From 7628a5cefaf8d6933097e239d4c818fca4a2a9bb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Jun 2020 16:09:23 +0200 Subject: [PATCH 068/131] removed vendors from pyblish gui --- pype/tools/pyblish_pype/vendor/Qt.py | 1827 ------------------------- pype/tools/pyblish_pype/vendor/six.py | 868 ------------ 2 files changed, 2695 deletions(-) delete mode 100644 pype/tools/pyblish_pype/vendor/Qt.py delete mode 100644 pype/tools/pyblish_pype/vendor/six.py diff --git a/pype/tools/pyblish_pype/vendor/Qt.py b/pype/tools/pyblish_pype/vendor/Qt.py deleted file mode 100644 index 841c823c5c..0000000000 --- a/pype/tools/pyblish_pype/vendor/Qt.py +++ /dev/null @@ -1,1827 +0,0 @@ -"""Minimal Python 2 & 3 shim around all Qt bindings - -DOCUMENTATION - Qt.py was born in the film and visual effects industry to address - the growing need for the development of software capable of running - with more than one flavour of the Qt bindings for Python - PySide, - PySide2, PyQt4 and PyQt5. - - 1. Build for one, run with all - 2. Explicit is better than implicit - 3. Support co-existence - - Default resolution order: - - PySide2 - - PyQt5 - - PySide - - PyQt4 - - Usage: - >> import sys - >> from Qt import QtWidgets - >> app = QtWidgets.QApplication(sys.argv) - >> button = QtWidgets.QPushButton("Hello World") - >> button.show() - >> app.exec_() - - All members of PySide2 are mapped from other bindings, should they exist. - If no equivalent member exist, it is excluded from Qt.py and inaccessible. - The idea is to highlight members that exist across all supported binding, - and guarantee that code that runs on one binding runs on all others. - - For more details, visit https://github.com/mottosso/Qt.py - -LICENSE - - See end of file for license (MIT, BSD) information. - -""" - -import os -import sys -import types -import shutil - - -__version__ = "1.2.0.b2" - -# Enable support for `from Qt import *` -__all__ = [] - -# Flags from environment variables -QT_VERBOSE = bool(os.getenv("QT_VERBOSE")) -QT_PREFERRED_BINDING = os.getenv("QT_PREFERRED_BINDING", "") -QT_SIP_API_HINT = os.getenv("QT_SIP_API_HINT") - -# Reference to Qt.py -Qt = sys.modules[__name__] -Qt.QtCompat = types.ModuleType("QtCompat") - -try: - long -except NameError: - # Python 3 compatibility - long = int - - -"""Common members of all bindings - -This is where each member of Qt.py is explicitly defined. -It is based on a "lowest common denominator" of all bindings; -including members found in each of the 4 bindings. - -The "_common_members" dictionary is generated using the -build_membership.sh script. - -""" - -_common_members = { - "QtCore": [ - "QAbstractAnimation", - "QAbstractEventDispatcher", - "QAbstractItemModel", - "QAbstractListModel", - "QAbstractState", - "QAbstractTableModel", - "QAbstractTransition", - "QAnimationGroup", - "QBasicTimer", - "QBitArray", - "QBuffer", - "QByteArray", - "QByteArrayMatcher", - "QChildEvent", - "QCoreApplication", - "QCryptographicHash", - "QDataStream", - "QDate", - "QDateTime", - "QDir", - "QDirIterator", - "QDynamicPropertyChangeEvent", - "QEasingCurve", - "QElapsedTimer", - "QEvent", - "QEventLoop", - "QEventTransition", - "QFile", - "QFileInfo", - "QFileSystemWatcher", - "QFinalState", - "QGenericArgument", - "QGenericReturnArgument", - "QHistoryState", - "QItemSelectionRange", - "QIODevice", - "QLibraryInfo", - "QLine", - "QLineF", - "QLocale", - "QMargins", - "QMetaClassInfo", - "QMetaEnum", - "QMetaMethod", - "QMetaObject", - "QMetaProperty", - "QMimeData", - "QModelIndex", - "QMutex", - "QMutexLocker", - "QObject", - "QParallelAnimationGroup", - "QPauseAnimation", - "QPersistentModelIndex", - "QPluginLoader", - "QPoint", - "QPointF", - "QProcess", - "QProcessEnvironment", - "QPropertyAnimation", - "QReadLocker", - "QReadWriteLock", - "QRect", - "QRectF", - "QRegExp", - "QResource", - "QRunnable", - "QSemaphore", - "QSequentialAnimationGroup", - "QSettings", - "QSignalMapper", - "QSignalTransition", - "QSize", - "QSizeF", - "QSocketNotifier", - "QState", - "QStateMachine", - "QSysInfo", - "QSystemSemaphore", - "QT_TRANSLATE_NOOP", - "QT_TR_NOOP", - "QT_TR_NOOP_UTF8", - "QTemporaryFile", - "QTextBoundaryFinder", - "QTextCodec", - "QTextDecoder", - "QTextEncoder", - "QTextStream", - "QTextStreamManipulator", - "QThread", - "QThreadPool", - "QTime", - "QTimeLine", - "QTimer", - "QTimerEvent", - "QTranslator", - "QUrl", - "QVariantAnimation", - "QWaitCondition", - "QWriteLocker", - "QXmlStreamAttribute", - "QXmlStreamAttributes", - "QXmlStreamEntityDeclaration", - "QXmlStreamEntityResolver", - "QXmlStreamNamespaceDeclaration", - "QXmlStreamNotationDeclaration", - "QXmlStreamReader", - "QXmlStreamWriter", - "Qt", - "QtCriticalMsg", - "QtDebugMsg", - "QtFatalMsg", - "QtMsgType", - "QtSystemMsg", - "QtWarningMsg", - "qAbs", - "qAddPostRoutine", - "qChecksum", - "qCritical", - "qDebug", - "qFatal", - "qFuzzyCompare", - "qIsFinite", - "qIsInf", - "qIsNaN", - "qIsNull", - "qRegisterResourceData", - "qUnregisterResourceData", - "qVersion", - "qWarning", - "qrand", - "qsrand" - ], - "QtGui": [ - "QAbstractTextDocumentLayout", - "QActionEvent", - "QBitmap", - "QBrush", - "QClipboard", - "QCloseEvent", - "QColor", - "QConicalGradient", - "QContextMenuEvent", - "QCursor", - "QDesktopServices", - "QDoubleValidator", - "QDrag", - "QDragEnterEvent", - "QDragLeaveEvent", - "QDragMoveEvent", - "QDropEvent", - "QFileOpenEvent", - "QFocusEvent", - "QFont", - "QFontDatabase", - "QFontInfo", - "QFontMetrics", - "QFontMetricsF", - "QGradient", - "QHelpEvent", - "QHideEvent", - "QHoverEvent", - "QIcon", - "QIconDragEvent", - "QIconEngine", - "QImage", - "QImageIOHandler", - "QImageReader", - "QImageWriter", - "QInputEvent", - "QInputMethodEvent", - "QIntValidator", - "QKeyEvent", - "QKeySequence", - "QLinearGradient", - "QMatrix2x2", - "QMatrix2x3", - "QMatrix2x4", - "QMatrix3x2", - "QMatrix3x3", - "QMatrix3x4", - "QMatrix4x2", - "QMatrix4x3", - "QMatrix4x4", - "QMouseEvent", - "QMoveEvent", - "QMovie", - "QPaintDevice", - "QPaintEngine", - "QPaintEngineState", - "QPaintEvent", - "QPainter", - "QPainterPath", - "QPainterPathStroker", - "QPalette", - "QPen", - "QPicture", - "QPictureIO", - "QPixmap", - "QPixmapCache", - "QPolygon", - "QPolygonF", - "QQuaternion", - "QRadialGradient", - "QRegExpValidator", - "QRegion", - "QResizeEvent", - "QSessionManager", - "QShortcutEvent", - "QShowEvent", - "QStandardItem", - "QStandardItemModel", - "QStatusTipEvent", - "QSyntaxHighlighter", - "QTabletEvent", - "QTextBlock", - "QTextBlockFormat", - "QTextBlockGroup", - "QTextBlockUserData", - "QTextCharFormat", - "QTextCursor", - "QTextDocument", - "QTextDocumentFragment", - "QTextFormat", - "QTextFragment", - "QTextFrame", - "QTextFrameFormat", - "QTextImageFormat", - "QTextInlineObject", - "QTextItem", - "QTextLayout", - "QTextLength", - "QTextLine", - "QTextList", - "QTextListFormat", - "QTextObject", - "QTextObjectInterface", - "QTextOption", - "QTextTable", - "QTextTableCell", - "QTextTableCellFormat", - "QTextTableFormat", - "QTouchEvent", - "QTransform", - "QValidator", - "QVector2D", - "QVector3D", - "QVector4D", - "QWhatsThisClickedEvent", - "QWheelEvent", - "QWindowStateChangeEvent", - "qAlpha", - "qBlue", - "qGray", - "qGreen", - "qIsGray", - "qRed", - "qRgb", - "qRgba" - ], - "QtHelp": [ - "QHelpContentItem", - "QHelpContentModel", - "QHelpContentWidget", - "QHelpEngine", - "QHelpEngineCore", - "QHelpIndexModel", - "QHelpIndexWidget", - "QHelpSearchEngine", - "QHelpSearchQuery", - "QHelpSearchQueryWidget", - "QHelpSearchResultWidget" - ], - "QtMultimedia": [ - "QAbstractVideoBuffer", - "QAbstractVideoSurface", - "QAudio", - "QAudioDeviceInfo", - "QAudioFormat", - "QAudioInput", - "QAudioOutput", - "QVideoFrame", - "QVideoSurfaceFormat" - ], - "QtNetwork": [ - "QAbstractNetworkCache", - "QAbstractSocket", - "QAuthenticator", - "QHostAddress", - "QHostInfo", - "QLocalServer", - "QLocalSocket", - "QNetworkAccessManager", - "QNetworkAddressEntry", - "QNetworkCacheMetaData", - "QNetworkConfiguration", - "QNetworkConfigurationManager", - "QNetworkCookie", - "QNetworkCookieJar", - "QNetworkDiskCache", - "QNetworkInterface", - "QNetworkProxy", - "QNetworkProxyFactory", - "QNetworkProxyQuery", - "QNetworkReply", - "QNetworkRequest", - "QNetworkSession", - "QSsl", - "QTcpServer", - "QTcpSocket", - "QUdpSocket" - ], - "QtOpenGL": [ - "QGL", - "QGLContext", - "QGLFormat", - "QGLWidget" - ], - "QtPrintSupport": [ - "QAbstractPrintDialog", - "QPageSetupDialog", - "QPrintDialog", - "QPrintEngine", - "QPrintPreviewDialog", - "QPrintPreviewWidget", - "QPrinter", - "QPrinterInfo" - ], - "QtSql": [ - "QSql", - "QSqlDatabase", - "QSqlDriver", - "QSqlDriverCreatorBase", - "QSqlError", - "QSqlField", - "QSqlIndex", - "QSqlQuery", - "QSqlQueryModel", - "QSqlRecord", - "QSqlRelation", - "QSqlRelationalDelegate", - "QSqlRelationalTableModel", - "QSqlResult", - "QSqlTableModel" - ], - "QtSvg": [ - "QGraphicsSvgItem", - "QSvgGenerator", - "QSvgRenderer", - "QSvgWidget" - ], - "QtTest": [ - "QTest" - ], - "QtWidgets": [ - "QAbstractButton", - "QAbstractGraphicsShapeItem", - "QAbstractItemDelegate", - "QAbstractItemView", - "QAbstractScrollArea", - "QAbstractSlider", - "QAbstractSpinBox", - "QAction", - "QActionGroup", - "QApplication", - "QBoxLayout", - "QButtonGroup", - "QCalendarWidget", - "QCheckBox", - "QColorDialog", - "QColumnView", - "QComboBox", - "QCommandLinkButton", - "QCommonStyle", - "QCompleter", - "QDataWidgetMapper", - "QDateEdit", - "QDateTimeEdit", - "QDesktopWidget", - "QDial", - "QDialog", - "QDialogButtonBox", - "QDirModel", - "QDockWidget", - "QDoubleSpinBox", - "QErrorMessage", - "QFileDialog", - "QFileIconProvider", - "QFileSystemModel", - "QFocusFrame", - "QFontComboBox", - "QFontDialog", - "QFormLayout", - "QFrame", - "QGesture", - "QGestureEvent", - "QGestureRecognizer", - "QGraphicsAnchor", - "QGraphicsAnchorLayout", - "QGraphicsBlurEffect", - "QGraphicsColorizeEffect", - "QGraphicsDropShadowEffect", - "QGraphicsEffect", - "QGraphicsEllipseItem", - "QGraphicsGridLayout", - "QGraphicsItem", - "QGraphicsItemGroup", - "QGraphicsLayout", - "QGraphicsLayoutItem", - "QGraphicsLineItem", - "QGraphicsLinearLayout", - "QGraphicsObject", - "QGraphicsOpacityEffect", - "QGraphicsPathItem", - "QGraphicsPixmapItem", - "QGraphicsPolygonItem", - "QGraphicsProxyWidget", - "QGraphicsRectItem", - "QGraphicsRotation", - "QGraphicsScale", - "QGraphicsScene", - "QGraphicsSceneContextMenuEvent", - "QGraphicsSceneDragDropEvent", - "QGraphicsSceneEvent", - "QGraphicsSceneHelpEvent", - "QGraphicsSceneHoverEvent", - "QGraphicsSceneMouseEvent", - "QGraphicsSceneMoveEvent", - "QGraphicsSceneResizeEvent", - "QGraphicsSceneWheelEvent", - "QGraphicsSimpleTextItem", - "QGraphicsTextItem", - "QGraphicsTransform", - "QGraphicsView", - "QGraphicsWidget", - "QGridLayout", - "QGroupBox", - "QHBoxLayout", - "QHeaderView", - "QInputDialog", - "QItemDelegate", - "QItemEditorCreatorBase", - "QItemEditorFactory", - "QKeyEventTransition", - "QLCDNumber", - "QLabel", - "QLayout", - "QLayoutItem", - "QLineEdit", - "QListView", - "QListWidget", - "QListWidgetItem", - "QMainWindow", - "QMdiArea", - "QMdiSubWindow", - "QMenu", - "QMenuBar", - "QMessageBox", - "QMouseEventTransition", - "QPanGesture", - "QPinchGesture", - "QPlainTextDocumentLayout", - "QPlainTextEdit", - "QProgressBar", - "QProgressDialog", - "QPushButton", - "QRadioButton", - "QRubberBand", - "QScrollArea", - "QScrollBar", - "QShortcut", - "QSizeGrip", - "QSizePolicy", - "QSlider", - "QSpacerItem", - "QSpinBox", - "QSplashScreen", - "QSplitter", - "QSplitterHandle", - "QStackedLayout", - "QStackedWidget", - "QStatusBar", - "QStyle", - "QStyleFactory", - "QStyleHintReturn", - "QStyleHintReturnMask", - "QStyleHintReturnVariant", - "QStyleOption", - "QStyleOptionButton", - "QStyleOptionComboBox", - "QStyleOptionComplex", - "QStyleOptionDockWidget", - "QStyleOptionFocusRect", - "QStyleOptionFrame", - "QStyleOptionGraphicsItem", - "QStyleOptionGroupBox", - "QStyleOptionHeader", - "QStyleOptionMenuItem", - "QStyleOptionProgressBar", - "QStyleOptionRubberBand", - "QStyleOptionSizeGrip", - "QStyleOptionSlider", - "QStyleOptionSpinBox", - "QStyleOptionTab", - "QStyleOptionTabBarBase", - "QStyleOptionTabWidgetFrame", - "QStyleOptionTitleBar", - "QStyleOptionToolBar", - "QStyleOptionToolBox", - "QStyleOptionToolButton", - "QStyleOptionViewItem", - "QStylePainter", - "QStyledItemDelegate", - "QSwipeGesture", - "QSystemTrayIcon", - "QTabBar", - "QTabWidget", - "QTableView", - "QTableWidget", - "QTableWidgetItem", - "QTableWidgetSelectionRange", - "QTapAndHoldGesture", - "QTapGesture", - "QTextBrowser", - "QTextEdit", - "QTimeEdit", - "QToolBar", - "QToolBox", - "QToolButton", - "QToolTip", - "QTreeView", - "QTreeWidget", - "QTreeWidgetItem", - "QTreeWidgetItemIterator", - "QUndoCommand", - "QUndoGroup", - "QUndoStack", - "QUndoView", - "QVBoxLayout", - "QWhatsThis", - "QWidget", - "QWidgetAction", - "QWidgetItem", - "QWizard", - "QWizardPage" - ], - "QtX11Extras": [ - "QX11Info" - ], - "QtXml": [ - "QDomAttr", - "QDomCDATASection", - "QDomCharacterData", - "QDomComment", - "QDomDocument", - "QDomDocumentFragment", - "QDomDocumentType", - "QDomElement", - "QDomEntity", - "QDomEntityReference", - "QDomImplementation", - "QDomNamedNodeMap", - "QDomNode", - "QDomNodeList", - "QDomNotation", - "QDomProcessingInstruction", - "QDomText", - "QXmlAttributes", - "QXmlContentHandler", - "QXmlDTDHandler", - "QXmlDeclHandler", - "QXmlDefaultHandler", - "QXmlEntityResolver", - "QXmlErrorHandler", - "QXmlInputSource", - "QXmlLexicalHandler", - "QXmlLocator", - "QXmlNamespaceSupport", - "QXmlParseException", - "QXmlReader", - "QXmlSimpleReader" - ], - "QtXmlPatterns": [ - "QAbstractMessageHandler", - "QAbstractUriResolver", - "QAbstractXmlNodeModel", - "QAbstractXmlReceiver", - "QSourceLocation", - "QXmlFormatter", - "QXmlItem", - "QXmlName", - "QXmlNamePool", - "QXmlNodeModelIndex", - "QXmlQuery", - "QXmlResultItems", - "QXmlSchema", - "QXmlSchemaValidator", - "QXmlSerializer" - ] -} - - -def _qInstallMessageHandler(handler): - """Install a message handler that works in all bindings - - Args: - handler: A function that takes 3 arguments, or None - """ - def messageOutputHandler(*args): - # In Qt4 bindings, message handlers are passed 2 arguments - # In Qt5 bindings, message handlers are passed 3 arguments - # The first argument is a QtMsgType - # The last argument is the message to be printed - # The Middle argument (if passed) is a QMessageLogContext - if len(args) == 3: - msgType, logContext, msg = args - elif len(args) == 2: - msgType, msg = args - logContext = None - else: - raise TypeError( - "handler expected 2 or 3 arguments, got {0}".format(len(args))) - - if isinstance(msg, bytes): - # In python 3, some bindings pass a bytestring, which cannot be - # used elsewhere. Decoding a python 2 or 3 bytestring object will - # consistently return a unicode object. - msg = msg.decode() - - handler(msgType, logContext, msg) - - passObject = messageOutputHandler if handler else handler - if Qt.IsPySide or Qt.IsPyQt4: - return Qt._QtCore.qInstallMsgHandler(passObject) - elif Qt.IsPySide2 or Qt.IsPyQt5: - return Qt._QtCore.qInstallMessageHandler(passObject) - - -def _getcpppointer(object): - if hasattr(Qt, "_shiboken2"): - return getattr(Qt, "_shiboken2").getCppPointer(object)[0] - elif hasattr(Qt, "_shiboken"): - return getattr(Qt, "_shiboken").getCppPointer(object)[0] - elif hasattr(Qt, "_sip"): - return getattr(Qt, "_sip").unwrapinstance(object) - raise AttributeError("'module' has no attribute 'getCppPointer'") - - -def _wrapinstance(ptr, base=None): - """Enable implicit cast of pointer to most suitable class - - This behaviour is available in sip per default. - - Based on http://nathanhorne.com/pyqtpyside-wrap-instance - - Usage: - This mechanism kicks in under these circumstances. - 1. Qt.py is using PySide 1 or 2. - 2. A `base` argument is not provided. - - See :func:`QtCompat.wrapInstance()` - - Arguments: - ptr (long): Pointer to QObject in memory - base (QObject, optional): Base class to wrap with. Defaults to QObject, - which should handle anything. - - """ - - assert isinstance(ptr, long), "Argument 'ptr' must be of type " - assert (base is None) or issubclass(base, Qt.QtCore.QObject), ( - "Argument 'base' must be of type ") - - if Qt.IsPyQt4 or Qt.IsPyQt5: - func = getattr(Qt, "_sip").wrapinstance - elif Qt.IsPySide2: - func = getattr(Qt, "_shiboken2").wrapInstance - elif Qt.IsPySide: - func = getattr(Qt, "_shiboken").wrapInstance - else: - raise AttributeError("'module' has no attribute 'wrapInstance'") - - if base is None: - q_object = func(long(ptr), Qt.QtCore.QObject) - meta_object = q_object.metaObject() - class_name = meta_object.className() - super_class_name = meta_object.superClass().className() - - if hasattr(Qt.QtWidgets, class_name): - base = getattr(Qt.QtWidgets, class_name) - - elif hasattr(Qt.QtWidgets, super_class_name): - base = getattr(Qt.QtWidgets, super_class_name) - - else: - base = Qt.QtCore.QObject - - return func(long(ptr), base) - - -def _translate(context, sourceText, *args): - # In Qt4 bindings, translate can be passed 2 or 3 arguments - # In Qt5 bindings, translate can be passed 2 arguments - # The first argument is disambiguation[str] - # The last argument is n[int] - # The middle argument can be encoding[QtCore.QCoreApplication.Encoding] - if len(args) == 3: - disambiguation, encoding, n = args - elif len(args) == 2: - disambiguation, n = args - encoding = None - else: - raise TypeError( - "Expected 4 or 5 arguments, got {0}.".format(len(args)+2)) - - if hasattr(Qt.QtCore, "QCoreApplication"): - app = getattr(Qt.QtCore, "QCoreApplication") - else: - raise NotImplementedError( - "Missing QCoreApplication implementation for {binding}".format( - binding=Qt.__binding__, - ) - ) - if Qt.__binding__ in ("PySide2", "PyQt5"): - sanitized_args = [context, sourceText, disambiguation, n] - else: - sanitized_args = [ - context, - sourceText, - disambiguation, - encoding or app.CodecForTr, - n - ] - return app.translate(*sanitized_args) - - -def _loadUi(uifile, baseinstance=None): - """Dynamically load a user interface from the given `uifile` - - This function calls `uic.loadUi` if using PyQt bindings, - else it implements a comparable binding for PySide. - - Documentation: - http://pyqt.sourceforge.net/Docs/PyQt5/designer.html#PyQt5.uic.loadUi - - Arguments: - uifile (str): Absolute path to Qt Designer file. - baseinstance (QWidget): Instantiated QWidget or subclass thereof - - Return: - baseinstance if `baseinstance` is not `None`. Otherwise - return the newly created instance of the user interface. - - """ - if hasattr(Qt, "_uic"): - return Qt._uic.loadUi(uifile, baseinstance) - - elif hasattr(Qt, "_QtUiTools"): - # Implement `PyQt5.uic.loadUi` for PySide(2) - - class _UiLoader(Qt._QtUiTools.QUiLoader): - """Create the user interface in a base instance. - - Unlike `Qt._QtUiTools.QUiLoader` itself this class does not - create a new instance of the top-level widget, but creates the user - interface in an existing instance of the top-level class if needed. - - This mimics the behaviour of `PyQt5.uic.loadUi`. - - """ - - def __init__(self, baseinstance): - super(_UiLoader, self).__init__(baseinstance) - self.baseinstance = baseinstance - - def load(self, uifile, *args, **kwargs): - from xml.etree.ElementTree import ElementTree - - # For whatever reason, if this doesn't happen then - # reading an invalid or non-existing .ui file throws - # a RuntimeError. - etree = ElementTree() - etree.parse(uifile) - - widget = Qt._QtUiTools.QUiLoader.load( - self, uifile, *args, **kwargs) - - # Workaround for PySide 1.0.9, see issue #208 - widget.parentWidget() - - return widget - - def createWidget(self, class_name, parent=None, name=""): - """Called for each widget defined in ui file - - Overridden here to populate `baseinstance` instead. - - """ - - if parent is None and self.baseinstance: - # Supposed to create the top-level widget, - # return the base instance instead - return self.baseinstance - - # For some reason, Line is not in the list of available - # widgets, but works fine, so we have to special case it here. - if class_name in self.availableWidgets() + ["Line"]: - # Create a new widget for child widgets - widget = Qt._QtUiTools.QUiLoader.createWidget(self, - class_name, - parent, - name) - - else: - raise Exception("Custom widget '%s' not supported" - % class_name) - - if self.baseinstance: - # Set an attribute for the new child widget on the base - # instance, just like PyQt5.uic.loadUi does. - setattr(self.baseinstance, name, widget) - - return widget - - widget = _UiLoader(baseinstance).load(uifile) - Qt.QtCore.QMetaObject.connectSlotsByName(widget) - - return widget - - else: - raise NotImplementedError("No implementation available for loadUi") - - -"""Misplaced members - -These members from the original submodule are misplaced relative PySide2 - -""" -_misplaced_members = { - "PySide2": { - "QtGui.QStringListModel": "QtCore.QStringListModel", - "QtCore.Property": "QtCore.Property", - "QtCore.Signal": "QtCore.Signal", - "QtCore.Slot": "QtCore.Slot", - "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel", - "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", - "QtCore.QItemSelection": "QtCore.QItemSelection", - "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel", - "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange", - "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi], - "shiboken2.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance], - "shiboken2.getCppPointer": ["QtCompat.getCppPointer", _getcpppointer], - "QtWidgets.qApp": "QtWidgets.QApplication.instance()", - "QtCore.QCoreApplication.translate": [ - "QtCompat.translate", _translate - ], - "QtWidgets.QApplication.translate": [ - "QtCompat.translate", _translate - ], - "QtCore.qInstallMessageHandler": [ - "QtCompat.qInstallMessageHandler", _qInstallMessageHandler - ], - }, - "PyQt5": { - "QtCore.pyqtProperty": "QtCore.Property", - "QtCore.pyqtSignal": "QtCore.Signal", - "QtCore.pyqtSlot": "QtCore.Slot", - "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel", - "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", - "QtCore.QStringListModel": "QtCore.QStringListModel", - "QtCore.QItemSelection": "QtCore.QItemSelection", - "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel", - "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange", - "uic.loadUi": ["QtCompat.loadUi", _loadUi], - "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance], - "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer], - "QtWidgets.qApp": "QtWidgets.QApplication.instance()", - "QtCore.QCoreApplication.translate": [ - "QtCompat.translate", _translate - ], - "QtWidgets.QApplication.translate": [ - "QtCompat.translate", _translate - ], - "QtCore.qInstallMessageHandler": [ - "QtCompat.qInstallMessageHandler", _qInstallMessageHandler - ], - }, - "PySide": { - "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel", - "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", - "QtGui.QStringListModel": "QtCore.QStringListModel", - "QtGui.QItemSelection": "QtCore.QItemSelection", - "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel", - "QtCore.Property": "QtCore.Property", - "QtCore.Signal": "QtCore.Signal", - "QtCore.Slot": "QtCore.Slot", - "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange", - "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog", - "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog", - "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog", - "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine", - "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog", - "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget", - "QtGui.QPrinter": "QtPrintSupport.QPrinter", - "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo", - "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi], - "shiboken.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance], - "shiboken.unwrapInstance": ["QtCompat.getCppPointer", _getcpppointer], - "QtGui.qApp": "QtWidgets.QApplication.instance()", - "QtCore.QCoreApplication.translate": [ - "QtCompat.translate", _translate - ], - "QtGui.QApplication.translate": [ - "QtCompat.translate", _translate - ], - "QtCore.qInstallMsgHandler": [ - "QtCompat.qInstallMessageHandler", _qInstallMessageHandler - ], - }, - "PyQt4": { - "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel", - "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", - "QtGui.QItemSelection": "QtCore.QItemSelection", - "QtGui.QStringListModel": "QtCore.QStringListModel", - "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel", - "QtCore.pyqtProperty": "QtCore.Property", - "QtCore.pyqtSignal": "QtCore.Signal", - "QtCore.pyqtSlot": "QtCore.Slot", - "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange", - "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog", - "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog", - "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog", - "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine", - "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog", - "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget", - "QtGui.QPrinter": "QtPrintSupport.QPrinter", - "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo", - # "QtCore.pyqtSignature": "QtCore.Slot", - "uic.loadUi": ["QtCompat.loadUi", _loadUi], - "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance], - "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer], - "QtCore.QString": "str", - "QtGui.qApp": "QtWidgets.QApplication.instance()", - "QtCore.QCoreApplication.translate": [ - "QtCompat.translate", _translate - ], - "QtGui.QApplication.translate": [ - "QtCompat.translate", _translate - ], - "QtCore.qInstallMsgHandler": [ - "QtCompat.qInstallMessageHandler", _qInstallMessageHandler - ], - } -} - -""" Compatibility Members - -This dictionary is used to build Qt.QtCompat objects that provide a consistent -interface for obsolete members, and differences in binding return values. - -{ - "binding": { - "classname": { - "targetname": "binding_namespace", - } - } -} -""" -_compatibility_members = { - "PySide2": { - "QWidget": { - "grab": "QtWidgets.QWidget.grab", - }, - "QHeaderView": { - "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable", - "setSectionsClickable": - "QtWidgets.QHeaderView.setSectionsClickable", - "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode", - "setSectionResizeMode": - "QtWidgets.QHeaderView.setSectionResizeMode", - "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable", - "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable", - }, - "QFileDialog": { - "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", - "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", - "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", - }, - }, - "PyQt5": { - "QWidget": { - "grab": "QtWidgets.QWidget.grab", - }, - "QHeaderView": { - "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable", - "setSectionsClickable": - "QtWidgets.QHeaderView.setSectionsClickable", - "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode", - "setSectionResizeMode": - "QtWidgets.QHeaderView.setSectionResizeMode", - "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable", - "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable", - }, - "QFileDialog": { - "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", - "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", - "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", - }, - }, - "PySide": { - "QWidget": { - "grab": "QtWidgets.QPixmap.grabWidget", - }, - "QHeaderView": { - "sectionsClickable": "QtWidgets.QHeaderView.isClickable", - "setSectionsClickable": "QtWidgets.QHeaderView.setClickable", - "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode", - "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode", - "sectionsMovable": "QtWidgets.QHeaderView.isMovable", - "setSectionsMovable": "QtWidgets.QHeaderView.setMovable", - }, - "QFileDialog": { - "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", - "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", - "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", - }, - }, - "PyQt4": { - "QWidget": { - "grab": "QtWidgets.QPixmap.grabWidget", - }, - "QHeaderView": { - "sectionsClickable": "QtWidgets.QHeaderView.isClickable", - "setSectionsClickable": "QtWidgets.QHeaderView.setClickable", - "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode", - "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode", - "sectionsMovable": "QtWidgets.QHeaderView.isMovable", - "setSectionsMovable": "QtWidgets.QHeaderView.setMovable", - }, - "QFileDialog": { - "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", - "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", - "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", - }, - }, -} - - -def _apply_site_config(): - try: - import QtSiteConfig - except ImportError: - # If no QtSiteConfig module found, no modifications - # to _common_members are needed. - pass - else: - # Provide the ability to modify the dicts used to build Qt.py - if hasattr(QtSiteConfig, 'update_members'): - QtSiteConfig.update_members(_common_members) - - if hasattr(QtSiteConfig, 'update_misplaced_members'): - QtSiteConfig.update_misplaced_members(members=_misplaced_members) - - if hasattr(QtSiteConfig, 'update_compatibility_members'): - QtSiteConfig.update_compatibility_members( - members=_compatibility_members) - - -def _new_module(name): - return types.ModuleType(__name__ + "." + name) - - -def _import_sub_module(module, name): - """import_sub_module will mimic the function of importlib.import_module""" - module = __import__(module.__name__ + "." + name) - for level in name.split("."): - module = getattr(module, level) - return module - - -def _setup(module, extras): - """Install common submodules""" - - Qt.__binding__ = module.__name__ - - for name in list(_common_members) + extras: - try: - submodule = _import_sub_module( - module, name) - except ImportError: - try: - # For extra modules like sip and shiboken that may not be - # children of the binding. - submodule = __import__(name) - except ImportError: - continue - - setattr(Qt, "_" + name, submodule) - - if name not in extras: - # Store reference to original binding, - # but don't store speciality modules - # such as uic or QtUiTools - setattr(Qt, name, _new_module(name)) - - -def _reassign_misplaced_members(binding): - """Apply misplaced members from `binding` to Qt.py - - Arguments: - binding (dict): Misplaced members - - """ - - for src, dst in _misplaced_members[binding].items(): - dst_value = None - - src_parts = src.split(".") - src_module = src_parts[0] - src_member = None - if len(src_parts) > 1: - src_member = src_parts[1:] - - if isinstance(dst, (list, tuple)): - dst, dst_value = dst - - dst_parts = dst.split(".") - dst_module = dst_parts[0] - dst_member = None - if len(dst_parts) > 1: - dst_member = dst_parts[1] - - # Get the member we want to store in the namesapce. - if not dst_value: - try: - _part = getattr(Qt, "_" + src_module) - while src_member: - member = src_member.pop(0) - _part = getattr(_part, member) - dst_value = _part - except AttributeError: - # If the member we want to store in the namespace does not - # exist, there is no need to continue. This can happen if a - # request was made to rename a member that didn't exist, for - # example if QtWidgets isn't available on the target platform. - _log("Misplaced member has no source: {}".format(src)) - continue - - try: - src_object = getattr(Qt, dst_module) - except AttributeError: - if dst_module not in _common_members: - # Only create the Qt parent module if its listed in - # _common_members. Without this check, if you remove QtCore - # from _common_members, the default _misplaced_members will add - # Qt.QtCore so it can add Signal, Slot, etc. - msg = 'Not creating missing member module "{m}" for "{c}"' - _log(msg.format(m=dst_module, c=dst_member)) - continue - # If the dst is valid but the Qt parent module does not exist - # then go ahead and create a new module to contain the member. - setattr(Qt, dst_module, _new_module(dst_module)) - src_object = getattr(Qt, dst_module) - # Enable direct import of the new module - sys.modules[__name__ + "." + dst_module] = src_object - - if not dst_value: - dst_value = getattr(Qt, "_" + src_module) - if src_member: - dst_value = getattr(dst_value, src_member) - - setattr( - src_object, - dst_member or dst_module, - dst_value - ) - - -def _build_compatibility_members(binding, decorators=None): - """Apply `binding` to QtCompat - - Arguments: - binding (str): Top level binding in _compatibility_members. - decorators (dict, optional): Provides the ability to decorate the - original Qt methods when needed by a binding. This can be used - to change the returned value to a standard value. The key should - be the classname, the value is a dict where the keys are the - target method names, and the values are the decorator functions. - - """ - - decorators = decorators or dict() - - # Allow optional site-level customization of the compatibility members. - # This method does not need to be implemented in QtSiteConfig. - try: - import QtSiteConfig - except ImportError: - pass - else: - if hasattr(QtSiteConfig, 'update_compatibility_decorators'): - QtSiteConfig.update_compatibility_decorators(binding, decorators) - - _QtCompat = type("QtCompat", (object,), {}) - - for classname, bindings in _compatibility_members[binding].items(): - attrs = {} - for target, binding in bindings.items(): - namespaces = binding.split('.') - try: - src_object = getattr(Qt, "_" + namespaces[0]) - except AttributeError as e: - _log("QtCompat: AttributeError: %s" % e) - # Skip reassignment of non-existing members. - # This can happen if a request was made to - # rename a member that didn't exist, for example - # if QtWidgets isn't available on the target platform. - continue - - # Walk down any remaining namespace getting the object assuming - # that if the first namespace exists the rest will exist. - for namespace in namespaces[1:]: - src_object = getattr(src_object, namespace) - - # decorate the Qt method if a decorator was provided. - if target in decorators.get(classname, []): - # staticmethod must be called on the decorated method to - # prevent a TypeError being raised when the decorated method - # is called. - src_object = staticmethod( - decorators[classname][target](src_object)) - - attrs[target] = src_object - - # Create the QtCompat class and install it into the namespace - compat_class = type(classname, (_QtCompat,), attrs) - setattr(Qt.QtCompat, classname, compat_class) - - -def _pyside2(): - """Initialise PySide2 - - These functions serve to test the existence of a binding - along with set it up in such a way that it aligns with - the final step; adding members from the original binding - to Qt.py - - """ - - import PySide2 as module - extras = ["QtUiTools"] - try: - try: - # Before merge of PySide and shiboken - import shiboken2 - except ImportError: - # After merge of PySide and shiboken, May 2017 - from PySide2 import shiboken2 - extras.append("shiboken2") - except ImportError: - pass - - _setup(module, extras) - Qt.__binding_version__ = module.__version__ - - if hasattr(Qt, "_shiboken2"): - Qt.QtCompat.wrapInstance = _wrapinstance - Qt.QtCompat.getCppPointer = _getcpppointer - - if hasattr(Qt, "_QtUiTools"): - Qt.QtCompat.loadUi = _loadUi - - if hasattr(Qt, "_QtCore"): - Qt.__qt_version__ = Qt._QtCore.qVersion() - - if hasattr(Qt, "_QtWidgets"): - Qt.QtCompat.setSectionResizeMode = \ - Qt._QtWidgets.QHeaderView.setSectionResizeMode - - _reassign_misplaced_members("PySide2") - _build_compatibility_members("PySide2") - - -def _pyside(): - """Initialise PySide""" - - import PySide as module - extras = ["QtUiTools"] - try: - try: - # Before merge of PySide and shiboken - import shiboken - except ImportError: - # After merge of PySide and shiboken, May 2017 - from PySide import shiboken - extras.append("shiboken") - except ImportError: - pass - - _setup(module, extras) - Qt.__binding_version__ = module.__version__ - - if hasattr(Qt, "_shiboken"): - Qt.QtCompat.wrapInstance = _wrapinstance - Qt.QtCompat.getCppPointer = _getcpppointer - - if hasattr(Qt, "_QtUiTools"): - Qt.QtCompat.loadUi = _loadUi - - if hasattr(Qt, "_QtGui"): - setattr(Qt, "QtWidgets", _new_module("QtWidgets")) - setattr(Qt, "_QtWidgets", Qt._QtGui) - if hasattr(Qt._QtGui, "QX11Info"): - setattr(Qt, "QtX11Extras", _new_module("QtX11Extras")) - Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info - - Qt.QtCompat.setSectionResizeMode = Qt._QtGui.QHeaderView.setResizeMode - - if hasattr(Qt, "_QtCore"): - Qt.__qt_version__ = Qt._QtCore.qVersion() - - _reassign_misplaced_members("PySide") - _build_compatibility_members("PySide") - - -def _pyqt5(): - """Initialise PyQt5""" - - import PyQt5 as module - extras = ["uic"] - try: - import sip - extras.append(sip.__name__) - except ImportError: - sip = None - - _setup(module, extras) - if hasattr(Qt, "_sip"): - Qt.QtCompat.wrapInstance = _wrapinstance - Qt.QtCompat.getCppPointer = _getcpppointer - - if hasattr(Qt, "_uic"): - Qt.QtCompat.loadUi = _loadUi - - if hasattr(Qt, "_QtCore"): - Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR - Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR - - if hasattr(Qt, "_QtWidgets"): - Qt.QtCompat.setSectionResizeMode = \ - Qt._QtWidgets.QHeaderView.setSectionResizeMode - - _reassign_misplaced_members("PyQt5") - _build_compatibility_members('PyQt5') - - -def _pyqt4(): - """Initialise PyQt4""" - - import sip - - # Validation of envivornment variable. Prevents an error if - # the variable is invalid since it's just a hint. - try: - hint = int(QT_SIP_API_HINT) - except TypeError: - hint = None # Variable was None, i.e. not set. - except ValueError: - raise ImportError("QT_SIP_API_HINT=%s must be a 1 or 2") - - for api in ("QString", - "QVariant", - "QDate", - "QDateTime", - "QTextStream", - "QTime", - "QUrl"): - try: - sip.setapi(api, hint or 2) - except AttributeError: - raise ImportError("PyQt4 < 4.6 isn't supported by Qt.py") - except ValueError: - actual = sip.getapi(api) - if not hint: - raise ImportError("API version already set to %d" % actual) - else: - # Having provided a hint indicates a soft constraint, one - # that doesn't throw an exception. - sys.stderr.write( - "Warning: API '%s' has already been set to %d.\n" - % (api, actual) - ) - - import PyQt4 as module - extras = ["uic"] - try: - import sip - extras.append(sip.__name__) - except ImportError: - sip = None - - _setup(module, extras) - if hasattr(Qt, "_sip"): - Qt.QtCompat.wrapInstance = _wrapinstance - Qt.QtCompat.getCppPointer = _getcpppointer - - if hasattr(Qt, "_uic"): - Qt.QtCompat.loadUi = _loadUi - - if hasattr(Qt, "_QtGui"): - setattr(Qt, "QtWidgets", _new_module("QtWidgets")) - setattr(Qt, "_QtWidgets", Qt._QtGui) - if hasattr(Qt._QtGui, "QX11Info"): - setattr(Qt, "QtX11Extras", _new_module("QtX11Extras")) - Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info - - Qt.QtCompat.setSectionResizeMode = \ - Qt._QtGui.QHeaderView.setResizeMode - - if hasattr(Qt, "_QtCore"): - Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR - Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR - - _reassign_misplaced_members("PyQt4") - - # QFileDialog QtCompat decorator - def _standardizeQFileDialog(some_function): - """Decorator that makes PyQt4 return conform to other bindings""" - def wrapper(*args, **kwargs): - ret = (some_function(*args, **kwargs)) - - # PyQt4 only returns the selected filename, force it to a - # standard return of the selected filename, and a empty string - # for the selected filter - return ret, '' - - wrapper.__doc__ = some_function.__doc__ - wrapper.__name__ = some_function.__name__ - - return wrapper - - decorators = { - "QFileDialog": { - "getOpenFileName": _standardizeQFileDialog, - "getOpenFileNames": _standardizeQFileDialog, - "getSaveFileName": _standardizeQFileDialog, - } - } - _build_compatibility_members('PyQt4', decorators) - - -def _none(): - """Internal option (used in installer)""" - - Mock = type("Mock", (), {"__getattr__": lambda Qt, attr: None}) - - Qt.__binding__ = "None" - Qt.__qt_version__ = "0.0.0" - Qt.__binding_version__ = "0.0.0" - Qt.QtCompat.loadUi = lambda uifile, baseinstance=None: None - Qt.QtCompat.setSectionResizeMode = lambda *args, **kwargs: None - - for submodule in _common_members.keys(): - setattr(Qt, submodule, Mock()) - setattr(Qt, "_" + submodule, Mock()) - - -def _log(text): - if QT_VERBOSE: - sys.stdout.write(text + "\n") - - -def _convert(lines): - """Convert compiled .ui file from PySide2 to Qt.py - - Arguments: - lines (list): Each line of of .ui file - - Usage: - >> with open("myui.py") as f: - .. lines = _convert(f.readlines()) - - """ - - def parse(line): - line = line.replace("from PySide2 import", "from Qt import QtCompat,") - line = line.replace("QtWidgets.QApplication.translate", - "QtCompat.translate") - if "QtCore.SIGNAL" in line: - raise NotImplementedError("QtCore.SIGNAL is missing from PyQt5 " - "and so Qt.py does not support it: you " - "should avoid defining signals inside " - "your ui files.") - return line - - parsed = list() - for line in lines: - line = parse(line) - parsed.append(line) - - return parsed - - -def _cli(args): - """Qt.py command-line interface""" - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument("--convert", - help="Path to compiled Python module, e.g. my_ui.py") - parser.add_argument("--compile", - help="Accept raw .ui file and compile with native " - "PySide2 compiler.") - parser.add_argument("--stdout", - help="Write to stdout instead of file", - action="store_true") - parser.add_argument("--stdin", - help="Read from stdin instead of file", - action="store_true") - - args = parser.parse_args(args) - - if args.stdout: - raise NotImplementedError("--stdout") - - if args.stdin: - raise NotImplementedError("--stdin") - - if args.compile: - raise NotImplementedError("--compile") - - if args.convert: - sys.stdout.write("#\n" - "# WARNING: --convert is an ALPHA feature.\n#\n" - "# See https://github.com/mottosso/Qt.py/pull/132\n" - "# for details.\n" - "#\n") - - # - # ------> Read - # - with open(args.convert) as f: - lines = _convert(f.readlines()) - - backup = "%s_backup%s" % os.path.splitext(args.convert) - sys.stdout.write("Creating \"%s\"..\n" % backup) - shutil.copy(args.convert, backup) - - # - # <------ Write - # - with open(args.convert, "w") as f: - f.write("".join(lines)) - - sys.stdout.write("Successfully converted \"%s\"\n" % args.convert) - - -def _install(): - # Default order (customise order and content via QT_PREFERRED_BINDING) - default_order = ("PySide2", "PyQt5", "PySide", "PyQt4") - preferred_order = list( - b for b in QT_PREFERRED_BINDING.split(os.pathsep) if b - ) - - order = preferred_order or default_order - - available = { - "PySide2": _pyside2, - "PyQt5": _pyqt5, - "PySide": _pyside, - "PyQt4": _pyqt4, - "None": _none - } - - _log("Order: '%s'" % "', '".join(order)) - - # Allow site-level customization of the available modules. - _apply_site_config() - - found_binding = False - for name in order: - _log("Trying %s" % name) - - try: - available[name]() - found_binding = True - break - - except ImportError as e: - _log("ImportError: %s" % e) - - except KeyError: - _log("ImportError: Preferred binding '%s' not found." % name) - - if not found_binding: - # If not binding were found, throw this error - raise ImportError("No Qt binding were found.") - - # Install individual members - for name, members in _common_members.items(): - try: - their_submodule = getattr(Qt, "_%s" % name) - except AttributeError: - continue - - our_submodule = getattr(Qt, name) - - # Enable import * - __all__.append(name) - - # Enable direct import of submodule, - # e.g. import Qt.QtCore - sys.modules[__name__ + "." + name] = our_submodule - - for member in members: - # Accept that a submodule may miss certain members. - try: - their_member = getattr(their_submodule, member) - except AttributeError: - _log("'%s.%s' was missing." % (name, member)) - continue - - setattr(our_submodule, member, their_member) - - # Enable direct import of QtCompat - sys.modules['Qt.QtCompat'] = Qt.QtCompat - - # Backwards compatibility - if hasattr(Qt.QtCompat, 'loadUi'): - Qt.QtCompat.load_ui = Qt.QtCompat.loadUi - - -_install() - -# Setup Binding Enum states -Qt.IsPySide2 = Qt.__binding__ == 'PySide2' -Qt.IsPyQt5 = Qt.__binding__ == 'PyQt5' -Qt.IsPySide = Qt.__binding__ == 'PySide' -Qt.IsPyQt4 = Qt.__binding__ == 'PyQt4' - -"""Augment QtCompat - -QtCompat contains wrappers and added functionality -to the original bindings, such as the CLI interface -and otherwise incompatible members between bindings, -such as `QHeaderView.setSectionResizeMode`. - -""" - -Qt.QtCompat._cli = _cli -Qt.QtCompat._convert = _convert - -# Enable command-line interface -if __name__ == "__main__": - _cli(sys.argv[1:]) - - -# The MIT License (MIT) -# -# Copyright (c) 2016-2017 Marcus Ottosson -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# -# In PySide(2), loadUi does not exist, so we implement it -# -# `_UiLoader` is adapted from the qtpy project, which was further influenced -# by qt-helpers which was released under a 3-clause BSD license which in turn -# is based on a solution at: -# -# - https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8 -# -# The License for this code is as follows: -# -# qt-helpers - a common front-end to various Qt modules -# -# Copyright (c) 2015, Chris Beaumont and Thomas Robitaille -# -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the -# distribution. -# * Neither the name of the Glue project nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS -# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -# Which itself was based on the solution at -# -# https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8 -# -# which was released under the MIT license: -# -# Copyright (c) 2011 Sebastian Wiesner -# Modifications by Charl Botha -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files -# (the "Software"),to deal in the Software without restriction, -# including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pype/tools/pyblish_pype/vendor/six.py b/pype/tools/pyblish_pype/vendor/six.py deleted file mode 100644 index 190c0239cd..0000000000 --- a/pype/tools/pyblish_pype/vendor/six.py +++ /dev/null @@ -1,868 +0,0 @@ -"""Utilities for writing code that runs on Python 2 and 3""" - -# Copyright (c) 2010-2015 Benjamin Peterson -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from __future__ import absolute_import - -import functools -import itertools -import operator -import sys -import types - -__author__ = "Benjamin Peterson " -__version__ = "1.10.0" - - -# Useful for very coarse version differentiation. -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 -PY34 = sys.version_info[0:2] >= (3, 4) - -if PY3: - string_types = str, - integer_types = int, - class_types = type, - text_type = str - binary_type = bytes - - MAXSIZE = sys.maxsize -else: - string_types = basestring, - integer_types = (int, long) - class_types = (type, types.ClassType) - text_type = unicode - binary_type = str - - if sys.platform.startswith("java"): - # Jython always uses 32 bits. - MAXSIZE = int((1 << 31) - 1) - else: - # It's possible to have sizeof(long) != sizeof(Py_ssize_t). - class X(object): - - def __len__(self): - return 1 << 31 - try: - len(X()) - except OverflowError: - # 32-bit - MAXSIZE = int((1 << 31) - 1) - else: - # 64-bit - MAXSIZE = int((1 << 63) - 1) - del X - - -def _add_doc(func, doc): - """Add documentation to a function.""" - func.__doc__ = doc - - -def _import_module(name): - """Import module, returning the module after the last dot.""" - __import__(name) - return sys.modules[name] - - -class _LazyDescr(object): - - def __init__(self, name): - self.name = name - - def __get__(self, obj, tp): - result = self._resolve() - setattr(obj, self.name, result) # Invokes __set__. - try: - # This is a bit ugly, but it avoids running this again by - # removing this descriptor. - delattr(obj.__class__, self.name) - except AttributeError: - pass - return result - - -class MovedModule(_LazyDescr): - - def __init__(self, name, old, new=None): - super(MovedModule, self).__init__(name) - if PY3: - if new is None: - new = name - self.mod = new - else: - self.mod = old - - def _resolve(self): - return _import_module(self.mod) - - def __getattr__(self, attr): - _module = self._resolve() - value = getattr(_module, attr) - setattr(self, attr, value) - return value - - -class _LazyModule(types.ModuleType): - - def __init__(self, name): - super(_LazyModule, self).__init__(name) - self.__doc__ = self.__class__.__doc__ - - def __dir__(self): - attrs = ["__doc__", "__name__"] - attrs += [attr.name for attr in self._moved_attributes] - return attrs - - # Subclasses should override this - _moved_attributes = [] - - -class MovedAttribute(_LazyDescr): - - def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): - super(MovedAttribute, self).__init__(name) - if PY3: - if new_mod is None: - new_mod = name - self.mod = new_mod - if new_attr is None: - if old_attr is None: - new_attr = name - else: - new_attr = old_attr - self.attr = new_attr - else: - self.mod = old_mod - if old_attr is None: - old_attr = name - self.attr = old_attr - - def _resolve(self): - module = _import_module(self.mod) - return getattr(module, self.attr) - - -class _SixMetaPathImporter(object): - - """ - A meta path importer to import six.moves and its submodules. - - This class implements a PEP302 finder and loader. It should be compatible - with Python 2.5 and all existing versions of Python3 - """ - - def __init__(self, six_module_name): - self.name = six_module_name - self.known_modules = {} - - def _add_module(self, mod, *fullnames): - for fullname in fullnames: - self.known_modules[self.name + "." + fullname] = mod - - def _get_module(self, fullname): - return self.known_modules[self.name + "." + fullname] - - def find_module(self, fullname, path=None): - if fullname in self.known_modules: - return self - return None - - def __get_module(self, fullname): - try: - return self.known_modules[fullname] - except KeyError: - raise ImportError("This loader does not know module " + fullname) - - def load_module(self, fullname): - try: - # in case of a reload - return sys.modules[fullname] - except KeyError: - pass - mod = self.__get_module(fullname) - if isinstance(mod, MovedModule): - mod = mod._resolve() - else: - mod.__loader__ = self - sys.modules[fullname] = mod - return mod - - def is_package(self, fullname): - """ - Return true, if the named module is a package. - - We need this method to get correct spec objects with - Python 3.4 (see PEP451) - """ - return hasattr(self.__get_module(fullname), "__path__") - - def get_code(self, fullname): - """Return None - - Required, if is_package is implemented""" - self.__get_module(fullname) # eventually raises ImportError - return None - get_source = get_code # same as get_code - -_importer = _SixMetaPathImporter(__name__) - - -class _MovedItems(_LazyModule): - - """Lazy loading of moved objects""" - __path__ = [] # mark as package - - -_moved_attributes = [ - MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), - MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), - MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), - MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), - MovedAttribute("intern", "__builtin__", "sys"), - MovedAttribute("map", "itertools", "builtins", "imap", "map"), - MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), - MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), - MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), - MovedAttribute("reduce", "__builtin__", "functools"), - MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), - MovedAttribute("StringIO", "StringIO", "io"), - MovedAttribute("UserDict", "UserDict", "collections"), - MovedAttribute("UserList", "UserList", "collections"), - MovedAttribute("UserString", "UserString", "collections"), - MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), - MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), - MovedModule("builtins", "__builtin__"), - MovedModule("configparser", "ConfigParser"), - MovedModule("copyreg", "copy_reg"), - MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), - MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), - MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), - MovedModule("http_cookies", "Cookie", "http.cookies"), - MovedModule("html_entities", "htmlentitydefs", "html.entities"), - MovedModule("html_parser", "HTMLParser", "html.parser"), - MovedModule("http_client", "httplib", "http.client"), - MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), - MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), - MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), - MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), - MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), - MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), - MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), - MovedModule("cPickle", "cPickle", "pickle"), - MovedModule("queue", "Queue"), - MovedModule("reprlib", "repr"), - MovedModule("socketserver", "SocketServer"), - MovedModule("_thread", "thread", "_thread"), - MovedModule("tkinter", "Tkinter"), - MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), - MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), - MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), - MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), - MovedModule("tkinter_tix", "Tix", "tkinter.tix"), - MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), - MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), - MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), - MovedModule("tkinter_colorchooser", "tkColorChooser", - "tkinter.colorchooser"), - MovedModule("tkinter_commondialog", "tkCommonDialog", - "tkinter.commondialog"), - MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), - MovedModule("tkinter_font", "tkFont", "tkinter.font"), - MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), - MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", - "tkinter.simpledialog"), - MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), - MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), - MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), - MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), - MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), - MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), -] -# Add windows specific modules. -if sys.platform == "win32": - _moved_attributes += [ - MovedModule("winreg", "_winreg"), - ] - -for attr in _moved_attributes: - setattr(_MovedItems, attr.name, attr) - if isinstance(attr, MovedModule): - _importer._add_module(attr, "moves." + attr.name) -del attr - -_MovedItems._moved_attributes = _moved_attributes - -moves = _MovedItems(__name__ + ".moves") -_importer._add_module(moves, "moves") - - -class Module_six_moves_urllib_parse(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_parse""" - - -_urllib_parse_moved_attributes = [ - MovedAttribute("ParseResult", "urlparse", "urllib.parse"), - MovedAttribute("SplitResult", "urlparse", "urllib.parse"), - MovedAttribute("parse_qs", "urlparse", "urllib.parse"), - MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), - MovedAttribute("urldefrag", "urlparse", "urllib.parse"), - MovedAttribute("urljoin", "urlparse", "urllib.parse"), - MovedAttribute("urlparse", "urlparse", "urllib.parse"), - MovedAttribute("urlsplit", "urlparse", "urllib.parse"), - MovedAttribute("urlunparse", "urlparse", "urllib.parse"), - MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), - MovedAttribute("quote", "urllib", "urllib.parse"), - MovedAttribute("quote_plus", "urllib", "urllib.parse"), - MovedAttribute("unquote", "urllib", "urllib.parse"), - MovedAttribute("unquote_plus", "urllib", "urllib.parse"), - MovedAttribute("urlencode", "urllib", "urllib.parse"), - MovedAttribute("splitquery", "urllib", "urllib.parse"), - MovedAttribute("splittag", "urllib", "urllib.parse"), - MovedAttribute("splituser", "urllib", "urllib.parse"), - MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), - MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), - MovedAttribute("uses_params", "urlparse", "urllib.parse"), - MovedAttribute("uses_query", "urlparse", "urllib.parse"), - MovedAttribute("uses_relative", "urlparse", "urllib.parse"), -] -for attr in _urllib_parse_moved_attributes: - setattr(Module_six_moves_urllib_parse, attr.name, attr) -del attr - -Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes - -_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), - "moves.urllib_parse", "moves.urllib.parse") - - -class Module_six_moves_urllib_error(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_error""" - - -_urllib_error_moved_attributes = [ - MovedAttribute("URLError", "urllib2", "urllib.error"), - MovedAttribute("HTTPError", "urllib2", "urllib.error"), - MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), -] -for attr in _urllib_error_moved_attributes: - setattr(Module_six_moves_urllib_error, attr.name, attr) -del attr - -Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes - -_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), - "moves.urllib_error", "moves.urllib.error") - - -class Module_six_moves_urllib_request(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_request""" - - -_urllib_request_moved_attributes = [ - MovedAttribute("urlopen", "urllib2", "urllib.request"), - MovedAttribute("install_opener", "urllib2", "urllib.request"), - MovedAttribute("build_opener", "urllib2", "urllib.request"), - MovedAttribute("pathname2url", "urllib", "urllib.request"), - MovedAttribute("url2pathname", "urllib", "urllib.request"), - MovedAttribute("getproxies", "urllib", "urllib.request"), - MovedAttribute("Request", "urllib2", "urllib.request"), - MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), - MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), - MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), - MovedAttribute("BaseHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), - MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), - MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), - MovedAttribute("FileHandler", "urllib2", "urllib.request"), - MovedAttribute("FTPHandler", "urllib2", "urllib.request"), - MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), - MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), - MovedAttribute("urlretrieve", "urllib", "urllib.request"), - MovedAttribute("urlcleanup", "urllib", "urllib.request"), - MovedAttribute("URLopener", "urllib", "urllib.request"), - MovedAttribute("FancyURLopener", "urllib", "urllib.request"), - MovedAttribute("proxy_bypass", "urllib", "urllib.request"), -] -for attr in _urllib_request_moved_attributes: - setattr(Module_six_moves_urllib_request, attr.name, attr) -del attr - -Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes - -_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), - "moves.urllib_request", "moves.urllib.request") - - -class Module_six_moves_urllib_response(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_response""" - - -_urllib_response_moved_attributes = [ - MovedAttribute("addbase", "urllib", "urllib.response"), - MovedAttribute("addclosehook", "urllib", "urllib.response"), - MovedAttribute("addinfo", "urllib", "urllib.response"), - MovedAttribute("addinfourl", "urllib", "urllib.response"), -] -for attr in _urllib_response_moved_attributes: - setattr(Module_six_moves_urllib_response, attr.name, attr) -del attr - -Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes - -_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), - "moves.urllib_response", "moves.urllib.response") - - -class Module_six_moves_urllib_robotparser(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_robotparser""" - - -_urllib_robotparser_moved_attributes = [ - MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), -] -for attr in _urllib_robotparser_moved_attributes: - setattr(Module_six_moves_urllib_robotparser, attr.name, attr) -del attr - -Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes - -_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), - "moves.urllib_robotparser", "moves.urllib.robotparser") - - -class Module_six_moves_urllib(types.ModuleType): - - """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" - __path__ = [] # mark as package - parse = _importer._get_module("moves.urllib_parse") - error = _importer._get_module("moves.urllib_error") - request = _importer._get_module("moves.urllib_request") - response = _importer._get_module("moves.urllib_response") - robotparser = _importer._get_module("moves.urllib_robotparser") - - def __dir__(self): - return ['parse', 'error', 'request', 'response', 'robotparser'] - -_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), - "moves.urllib") - - -def add_move(move): - """Add an item to six.moves.""" - setattr(_MovedItems, move.name, move) - - -def remove_move(name): - """Remove item from six.moves.""" - try: - delattr(_MovedItems, name) - except AttributeError: - try: - del moves.__dict__[name] - except KeyError: - raise AttributeError("no such move, %r" % (name,)) - - -if PY3: - _meth_func = "__func__" - _meth_self = "__self__" - - _func_closure = "__closure__" - _func_code = "__code__" - _func_defaults = "__defaults__" - _func_globals = "__globals__" -else: - _meth_func = "im_func" - _meth_self = "im_self" - - _func_closure = "func_closure" - _func_code = "func_code" - _func_defaults = "func_defaults" - _func_globals = "func_globals" - - -try: - advance_iterator = next -except NameError: - def advance_iterator(it): - return it.next() -next = advance_iterator - - -try: - callable = callable -except NameError: - def callable(obj): - return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) - - -if PY3: - def get_unbound_function(unbound): - return unbound - - create_bound_method = types.MethodType - - def create_unbound_method(func, cls): - return func - - Iterator = object -else: - def get_unbound_function(unbound): - return unbound.im_func - - def create_bound_method(func, obj): - return types.MethodType(func, obj, obj.__class__) - - def create_unbound_method(func, cls): - return types.MethodType(func, None, cls) - - class Iterator(object): - - def next(self): - return type(self).__next__(self) - - callable = callable -_add_doc(get_unbound_function, - """Get the function out of a possibly unbound function""") - - -get_method_function = operator.attrgetter(_meth_func) -get_method_self = operator.attrgetter(_meth_self) -get_function_closure = operator.attrgetter(_func_closure) -get_function_code = operator.attrgetter(_func_code) -get_function_defaults = operator.attrgetter(_func_defaults) -get_function_globals = operator.attrgetter(_func_globals) - - -if PY3: - def iterkeys(d, **kw): - return iter(d.keys(**kw)) - - def itervalues(d, **kw): - return iter(d.values(**kw)) - - def iteritems(d, **kw): - return iter(d.items(**kw)) - - def iterlists(d, **kw): - return iter(d.lists(**kw)) - - viewkeys = operator.methodcaller("keys") - - viewvalues = operator.methodcaller("values") - - viewitems = operator.methodcaller("items") -else: - def iterkeys(d, **kw): - return d.iterkeys(**kw) - - def itervalues(d, **kw): - return d.itervalues(**kw) - - def iteritems(d, **kw): - return d.iteritems(**kw) - - def iterlists(d, **kw): - return d.iterlists(**kw) - - viewkeys = operator.methodcaller("viewkeys") - - viewvalues = operator.methodcaller("viewvalues") - - viewitems = operator.methodcaller("viewitems") - -_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") -_add_doc(itervalues, "Return an iterator over the values of a dictionary.") -_add_doc(iteritems, - "Return an iterator over the (key, value) pairs of a dictionary.") -_add_doc(iterlists, - "Return an iterator over the (key, [values]) pairs of a dictionary.") - - -if PY3: - def b(s): - return s.encode("latin-1") - - def u(s): - return s - unichr = chr - import struct - int2byte = struct.Struct(">B").pack - del struct - byte2int = operator.itemgetter(0) - indexbytes = operator.getitem - iterbytes = iter - import io - StringIO = io.StringIO - BytesIO = io.BytesIO - _assertCountEqual = "assertCountEqual" - if sys.version_info[1] <= 1: - _assertRaisesRegex = "assertRaisesRegexp" - _assertRegex = "assertRegexpMatches" - else: - _assertRaisesRegex = "assertRaisesRegex" - _assertRegex = "assertRegex" -else: - def b(s): - return s - # Workaround for standalone backslash - - def u(s): - return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") - unichr = unichr - int2byte = chr - - def byte2int(bs): - return ord(bs[0]) - - def indexbytes(buf, i): - return ord(buf[i]) - iterbytes = functools.partial(itertools.imap, ord) - import StringIO - StringIO = BytesIO = StringIO.StringIO - _assertCountEqual = "assertItemsEqual" - _assertRaisesRegex = "assertRaisesRegexp" - _assertRegex = "assertRegexpMatches" -_add_doc(b, """Byte literal""") -_add_doc(u, """Text literal""") - - -def assertCountEqual(self, *args, **kwargs): - return getattr(self, _assertCountEqual)(*args, **kwargs) - - -def assertRaisesRegex(self, *args, **kwargs): - return getattr(self, _assertRaisesRegex)(*args, **kwargs) - - -def assertRegex(self, *args, **kwargs): - return getattr(self, _assertRegex)(*args, **kwargs) - - -if PY3: - exec_ = getattr(moves.builtins, "exec") - - def reraise(tp, value, tb=None): - if value is None: - value = tp() - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - -else: - def exec_(_code_, _globs_=None, _locs_=None): - """Execute code in a namespace.""" - if _globs_ is None: - frame = sys._getframe(1) - _globs_ = frame.f_globals - if _locs_ is None: - _locs_ = frame.f_locals - del frame - elif _locs_ is None: - _locs_ = _globs_ - exec("""exec _code_ in _globs_, _locs_""") - - exec_("""def reraise(tp, value, tb=None): - raise tp, value, tb -""") - - -if sys.version_info[:2] == (3, 2): - exec_("""def raise_from(value, from_value): - if from_value is None: - raise value - raise value from from_value -""") -elif sys.version_info[:2] > (3, 2): - exec_("""def raise_from(value, from_value): - raise value from from_value -""") -else: - def raise_from(value, from_value): - raise value - - -print_ = getattr(moves.builtins, "print", None) -if print_ is None: - def print_(*args, **kwargs): - """The new-style print function for Python 2.4 and 2.5.""" - fp = kwargs.pop("file", sys.stdout) - if fp is None: - return - - def write(data): - if not isinstance(data, basestring): - data = str(data) - # If the file has an encoding, encode unicode with it. - if (isinstance(fp, file) and - isinstance(data, unicode) and - fp.encoding is not None): - errors = getattr(fp, "errors", None) - if errors is None: - errors = "strict" - data = data.encode(fp.encoding, errors) - fp.write(data) - want_unicode = False - sep = kwargs.pop("sep", None) - if sep is not None: - if isinstance(sep, unicode): - want_unicode = True - elif not isinstance(sep, str): - raise TypeError("sep must be None or a string") - end = kwargs.pop("end", None) - if end is not None: - if isinstance(end, unicode): - want_unicode = True - elif not isinstance(end, str): - raise TypeError("end must be None or a string") - if kwargs: - raise TypeError("invalid keyword arguments to print()") - if not want_unicode: - for arg in args: - if isinstance(arg, unicode): - want_unicode = True - break - if want_unicode: - newline = unicode("\n") - space = unicode(" ") - else: - newline = "\n" - space = " " - if sep is None: - sep = space - if end is None: - end = newline - for i, arg in enumerate(args): - if i: - write(sep) - write(arg) - write(end) -if sys.version_info[:2] < (3, 3): - _print = print_ - - def print_(*args, **kwargs): - fp = kwargs.get("file", sys.stdout) - flush = kwargs.pop("flush", False) - _print(*args, **kwargs) - if flush and fp is not None: - fp.flush() - -_add_doc(reraise, """Reraise an exception.""") - -if sys.version_info[0:2] < (3, 4): - def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, - updated=functools.WRAPPER_UPDATES): - def wrapper(f): - f = functools.wraps(wrapped, assigned, updated)(f) - f.__wrapped__ = wrapped - return f - return wrapper -else: - wraps = functools.wraps - - -def with_metaclass(meta, *bases): - """Create a base class with a metaclass.""" - # This requires a bit of explanation: the basic idea is to make a dummy - # metaclass for one level of class instantiation that replaces itself with - # the actual metaclass. - class metaclass(meta): - - def __new__(cls, name, this_bases, d): - return meta(name, bases, d) - return type.__new__(metaclass, 'temporary_class', (), {}) - - -def add_metaclass(metaclass): - """Class decorator for creating a class with a metaclass.""" - def wrapper(cls): - orig_vars = cls.__dict__.copy() - slots = orig_vars.get('__slots__') - if slots is not None: - if isinstance(slots, str): - slots = [slots] - for slots_var in slots: - orig_vars.pop(slots_var) - orig_vars.pop('__dict__', None) - orig_vars.pop('__weakref__', None) - return metaclass(cls.__name__, cls.__bases__, orig_vars) - return wrapper - - -def python_2_unicode_compatible(klass): - """ - A decorator that defines __unicode__ and __str__ methods under Python 2. - Under Python 3 it does nothing. - - To support Python 2 and 3 with a single code base, define a __str__ method - returning text and apply this decorator to the class. - """ - if PY2: - if '__str__' not in klass.__dict__: - raise ValueError("@python_2_unicode_compatible cannot be applied " - "to %s because it doesn't define __str__()." % - klass.__name__) - klass.__unicode__ = klass.__str__ - klass.__str__ = lambda self: self.__unicode__().encode('utf-8') - return klass - - -# Complete the moves implementation. -# This code is at the end of this module to speed up module loading. -# Turn this module into a package. -__path__ = [] # required for PEP 302 and PEP 451 -__package__ = __name__ # see PEP 366 @ReservedAssignment -if globals().get("__spec__") is not None: - __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable -# Remove other six meta path importers, since they cause problems. This can -# happen if six is removed from sys.modules and then reloaded. (Setuptools does -# this for some reason.) -if sys.meta_path: - for i, importer in enumerate(sys.meta_path): - # Here's some real nastiness: Another "instance" of the six module might - # be floating around. Therefore, we can't use isinstance() to check for - # the six meta path importer, since the other six instance will have - # inserted an importer with different class. - if (type(importer).__name__ == "_SixMetaPathImporter" and - importer.name == __name__): - del sys.meta_path[i] - break - del i, importer -# Finally, add the importer to the meta path import hook. -sys.meta_path.append(_importer) From 1d20ff14341897d5a69709ef78073f41072a4b21 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Jun 2020 16:09:53 +0200 Subject: [PATCH 069/131] fixed imports of removed vendors --- pype/tools/pyblish_pype/app.py | 2 +- pype/tools/pyblish_pype/constants.py | 2 +- pype/tools/pyblish_pype/control.py | 2 +- pype/tools/pyblish_pype/delegate.py | 2 +- pype/tools/pyblish_pype/model.py | 8 ++++---- pype/tools/pyblish_pype/util.py | 4 ++-- pype/tools/pyblish_pype/vendor/qtawesome/animation.py | 2 +- pype/tools/pyblish_pype/vendor/qtawesome/iconic_font.py | 4 ++-- pype/tools/pyblish_pype/view.py | 2 +- pype/tools/pyblish_pype/widgets.py | 2 +- pype/tools/pyblish_pype/window.py | 2 +- 11 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pype/tools/pyblish_pype/app.py b/pype/tools/pyblish_pype/app.py index 8b77d2f93d..0f662d5b3e 100644 --- a/pype/tools/pyblish_pype/app.py +++ b/pype/tools/pyblish_pype/app.py @@ -5,7 +5,7 @@ import os import sys from . import compat, control, settings, util, window -from .vendor.Qt import QtCore, QtGui, QtWidgets +from Qt import QtCore, QtGui, QtWidgets self = sys.modules[__name__] diff --git a/pype/tools/pyblish_pype/constants.py b/pype/tools/pyblish_pype/constants.py index 077d93eec0..5395d1fd0a 100644 --- a/pype/tools/pyblish_pype/constants.py +++ b/pype/tools/pyblish_pype/constants.py @@ -1,4 +1,4 @@ -from .vendor.Qt import QtCore +from Qt import QtCore def flags(*args, **kwargs): diff --git a/pype/tools/pyblish_pype/control.py b/pype/tools/pyblish_pype/control.py index e64f3d5bfb..cd83752186 100644 --- a/pype/tools/pyblish_pype/control.py +++ b/pype/tools/pyblish_pype/control.py @@ -9,7 +9,7 @@ import os import sys import traceback -from .vendor.Qt import QtCore +from Qt import QtCore import pyblish.api import pyblish.util diff --git a/pype/tools/pyblish_pype/delegate.py b/pype/tools/pyblish_pype/delegate.py index 849495cdeb..e88835b81a 100644 --- a/pype/tools/pyblish_pype/delegate.py +++ b/pype/tools/pyblish_pype/delegate.py @@ -1,6 +1,6 @@ import platform -from .vendor.Qt import QtWidgets, QtGui, QtCore +from Qt import QtWidgets, QtGui, QtCore from . import model from .awesome import tags as awesome diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index 58ab3ed0b7..24c1fd9f2f 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -29,10 +29,10 @@ import pyblish from . import settings, util from .awesome import tags as awesome -from .vendor import Qt -from .vendor.Qt import QtCore, QtGui -from .vendor.six import text_type -from .vendor.six.moves import queue +import Qt +from Qt import QtCore, QtGui +from six import text_type +from six.moves import queue from .vendor import qtawesome from .constants import PluginStates, InstanceStates, GroupStates, Roles diff --git a/pype/tools/pyblish_pype/util.py b/pype/tools/pyblish_pype/util.py index 82bf4eb51d..d10e7a002a 100644 --- a/pype/tools/pyblish_pype/util.py +++ b/pype/tools/pyblish_pype/util.py @@ -7,8 +7,8 @@ import numbers import copy import collections -from .vendor.Qt import QtCore -from .vendor.six import text_type +from Qt import QtCore +from six import text_type import pyblish.api root = os.path.dirname(__file__) diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/animation.py b/pype/tools/pyblish_pype/vendor/qtawesome/animation.py index a9638d74b0..e2a701785a 100644 --- a/pype/tools/pyblish_pype/vendor/qtawesome/animation.py +++ b/pype/tools/pyblish_pype/vendor/qtawesome/animation.py @@ -1,4 +1,4 @@ -from ..Qt import QtCore +from Qt import QtCore class Spin: diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/iconic_font.py b/pype/tools/pyblish_pype/vendor/qtawesome/iconic_font.py index 70f5ec2dec..cd937d7e7f 100644 --- a/pype/tools/pyblish_pype/vendor/qtawesome/iconic_font.py +++ b/pype/tools/pyblish_pype/vendor/qtawesome/iconic_font.py @@ -5,8 +5,8 @@ from __future__ import print_function import json import os -from .. import six -from ..Qt import QtCore, QtGui +import six +from Qt import QtCore, QtGui _default_options = { diff --git a/pype/tools/pyblish_pype/view.py b/pype/tools/pyblish_pype/view.py index 86cefd4a55..ada19bc7d9 100644 --- a/pype/tools/pyblish_pype/view.py +++ b/pype/tools/pyblish_pype/view.py @@ -1,4 +1,4 @@ -from .vendor.Qt import QtCore, QtWidgets +from Qt import QtCore, QtWidgets from . import model from .constants import Roles diff --git a/pype/tools/pyblish_pype/widgets.py b/pype/tools/pyblish_pype/widgets.py index 3a09249a86..e81633f7a3 100644 --- a/pype/tools/pyblish_pype/widgets.py +++ b/pype/tools/pyblish_pype/widgets.py @@ -1,5 +1,5 @@ import sys -from .vendor.Qt import QtCore, QtWidgets, QtGui +from Qt import QtCore, QtWidgets, QtGui from . import model, delegate, view, awesome from .constants import PluginStates, InstanceStates, Roles diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 84003a88aa..8ae871665c 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -44,7 +44,7 @@ from functools import partial from . import delegate, model, settings, util, view, widgets from .awesome import tags as awesome -from .vendor.Qt import QtCore, QtGui, QtWidgets +from Qt import QtCore, QtGui, QtWidgets from .constants import ( PluginStates, PluginActionStates, InstanceStates, GroupStates, Roles ) From 5889f6d7a03ede295a2df821b4815b448896b363 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 Jun 2020 16:54:58 +0100 Subject: [PATCH 070/131] Integrate editorial files. --- pype/plugins/global/publish/integrate_new.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index f8429e8b58..d6111f95f5 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -83,7 +83,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "fbx", "textures", "action", - "harmony.template" + "harmony.template", + "editorial" ] exclude_families = ["clip"] db_representation_context_keys = [ From 703c8bacb33f1574c234de75e1b1a4fec20a7a8e Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 Jun 2020 16:56:47 +0100 Subject: [PATCH 071/131] Fix scene settings. --- pype/hosts/harmony/__init__.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index 6cc5d50eb6..cdb8b40777 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -9,9 +9,9 @@ from pype import lib def ensure_scene_settings(): asset_data = lib.get_asset()["data"] - fps = asset_data["fps"] - frame_start = asset_data["frameStart"] - frame_end = asset_data["frameEnd"] + fps = asset_data.get("fps") + frame_start = asset_data.get("frameStart") + frame_end = asset_data.get("frameEnd") resolution_width = asset_data.get("resolutionWidth") resolution_height = asset_data.get("resolutionHeight") @@ -53,16 +53,24 @@ def ensure_scene_settings(): { scene.setFrameRate(args[0]["fps"]); } - if (args[0]["frameStart"]) + if (args[0]["frameStart"] && args[0]["frameEnd"]) { - scene.setStartFrame(args[0]["frameStart"]); - } - if (args[0]["frameEnd"]) - { - scene.setStopFrame(args[0]["frameEnd"]); - frame.remove( - args[0]["frameEnd"], frame.numberOf() - args[0]["frameEnd"] - ); + var duration = args[0]["frameEnd"] - args[0]["frameStart"] + 1 + if (frame.numberOf() > duration) + { + frame.remove( + duration, frame.numberOf() - duration + ); + } + if (frame.numberOf() < duration) + { + frame.insert( + duration, duration - frame.numberOf() + ); + } + + scene.setStartFrame(1); + scene.setStopFrame(duration); } if (args[0]["resolutionWidth"] && args[0]["resolutionHeight"]) { From a6859fa7e5fa655d911a9f2b57118c93dc66a982 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 Jun 2020 17:08:59 +0100 Subject: [PATCH 072/131] Redundant templates loader. --- pype/plugins/harmony/load/load_template.py | 61 ---------------------- 1 file changed, 61 deletions(-) delete mode 100644 pype/plugins/harmony/load/load_template.py diff --git a/pype/plugins/harmony/load/load_template.py b/pype/plugins/harmony/load/load_template.py deleted file mode 100644 index 53e0ff1f07..0000000000 --- a/pype/plugins/harmony/load/load_template.py +++ /dev/null @@ -1,61 +0,0 @@ -import tempfile -import zipfile -import os -import shutil - -from avalon import api, harmony - - -class ImportTemplateLoader(api.Loader): - """Import templates.""" - - families = ["harmony.template"] - representations = ["*"] - label = "Import Template" - - def load(self, context, name=None, namespace=None, data=None): - # Make backdrops from metadata. - backdrops = context["representation"]["data"].get("backdrops", []) - - func = """function func(args) - { - Backdrop.addBackdrop("Top", args[0]); - } - func - """ - for backdrop in backdrops: - harmony.send({"function": func, "args": [backdrop]}) - - # Import template. - temp_dir = tempfile.mkdtemp() - zip_file = api.get_representation_path(context["representation"]) - template_path = os.path.join(temp_dir, "temp.tpl") - with zipfile.ZipFile(zip_file, "r") as zip_ref: - zip_ref.extractall(template_path) - - func = """function func(args) - { - var template_path = args[0]; - var drag_object = copyPaste.copyFromTemplate( - template_path, 0, 0, copyPaste.getCurrentCreateOptions() - ); - copyPaste.pasteNewNodes( - drag_object, "", copyPaste.getCurrentPasteOptions() - ); - } - func - """ - - func = """function func(args) - { - var template_path = args[0]; - var drag_object = copyPaste.pasteTemplateIntoGroup( - template_path, "Top", 1 - ); - } - func - """ - - harmony.send({"function": func, "args": [template_path]}) - - shutil.rmtree(temp_dir) From 750db58d39f68a2eb8862d141fbaef5ac12c7719 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 Jun 2020 17:19:18 +0100 Subject: [PATCH 073/131] Batch tasks action --- .../actions/action_batch_task_creation.py | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 pype/modules/ftrack/actions/action_batch_task_creation.py diff --git a/pype/modules/ftrack/actions/action_batch_task_creation.py b/pype/modules/ftrack/actions/action_batch_task_creation.py new file mode 100644 index 0000000000..89e2977aad --- /dev/null +++ b/pype/modules/ftrack/actions/action_batch_task_creation.py @@ -0,0 +1,164 @@ +""" +Taken from https://github.com/tokejepsen/ftrack-hooks/tree/master/batch_tasks +""" + +from pype.modules.ftrack.lib import BaseAction + + +class BatchTasksAction(BaseAction): + '''Batch Tasks action + `label` a descriptive string identifing your action. + `varaint` To group actions together, give them the same + label and specify a unique variant per action. + `identifier` a unique identifier for your action. + `description` a verbose descriptive text for you action + ''' + label = "Batch Tasks" + variant = None + identifier = "batch-tasks" + description = None + + def discover(self, session, entities, event): + '''Return true if we can handle the selected entities. + *session* is a `ftrack_api.Session` instance + *entities* is a list of tuples each containing the entity type and the + entity id. + If the entity is a hierarchical you will always get the entity + type TypedContext, once retrieved through a get operation you + will have the "real" entity type ie. example Shot, Sequence + or Asset Build. + *event* the unmodified original event + ''' + # Only discover the action if any selection is made. + if entities: + return True + + return False + + def get_task_form_items(self, session, number_of_tasks): + items = [] + + task_type_options = [ + {'label': task_type["name"], 'value': task_type["id"]} + for task_type in session.query("Type") + ] + + for index in range(0, number_of_tasks): + items.extend( + [ + { + 'value': '##Template for Task{0}##'.format( + index + ), + 'type': 'label' + }, + { + 'label': 'Type', + 'type': 'enumerator', + 'name': 'task_{0}_typeid'.format(index), + 'data': task_type_options + }, + { + 'label': 'Name', + 'type': 'text', + 'name': 'task_{0}_name'.format(index) + } + ] + ) + + return items + + def ensure_task(self, session, name, task_type, parent): + + # Query for existing task. + query = ( + 'Task where type.id is "{0}" and name is "{1}" ' + 'and parent.id is "{2}"' + ) + task = session.query( + query.format( + task_type["id"], + name, + parent["id"] + ) + ).first() + + # Create task. + if not task: + session.create( + "Task", + { + "name": name, + "type": task_type, + "parent": parent + } + ) + + def launch(self, session, entities, event): + '''Callback method for the custom action. + return either a bool ( True if successful or False if the action + failed ) or a dictionary with they keys `message` and `success`, the + message should be a string and will be displayed as feedback to the + user, success should be a bool, True if successful or False if the + action failed. + *session* is a `ftrack_api.Session` instance + *entities* is a list of tuples each containing the entity type and the + entity id. + If the entity is a hierarchical you will always get the entity + type TypedContext, once retrieved through a get operation you + will have the "real" entity type ie. example Shot, Sequence + or Asset Build. + *event* the unmodified original event + ''' + if 'values' in event['data']: + values = event['data']['values'] + if 'number_of_tasks' in values: + return { + 'success': True, + 'message': '', + 'items': self.get_task_form_items( + session, int(values['number_of_tasks']) + ) + } + else: + # Create tasks on each entity + for entity in entities: + for count in range(0, int(len(values.keys()) / 2)): + task_type = session.query( + 'Type where id is "{0}"'.format( + values["task_{0}_typeid".format(count)] + ) + ).one() + + # Get name, or assume task type in lower case as name. + name = values["task_{0}_name".format(count)] + if not name: + name = task_type["name"].lower() + + self.ensure_task(session, name, task_type, entity) + + session.commit() + + return { + 'success': True, + 'message': 'Action completed successfully' + } + + return { + 'success': True, + 'message': "", + 'items': [ + { + 'label': 'Number of tasks', + 'type': 'number', + 'name': 'number_of_tasks', + 'value': 2 + } + ] + } + + +def register(session, plugins_presets={}): + '''Register action. Called when used as an event plugin.''' + + BatchTasksAction(session, plugins_presets).register() From 1cf7e7c11442c536fbece8a4420b255d816832cb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Jun 2020 18:50:42 +0200 Subject: [PATCH 074/131] added missing variable to sync event class --- pype/modules/ftrack/events/event_sync_to_avalon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/modules/ftrack/events/event_sync_to_avalon.py b/pype/modules/ftrack/events/event_sync_to_avalon.py index e79fcac4cd..739ec69522 100644 --- a/pype/modules/ftrack/events/event_sync_to_avalon.py +++ b/pype/modules/ftrack/events/event_sync_to_avalon.py @@ -45,6 +45,7 @@ class SyncToAvalonEvent(BaseEvent): " where project_id is \"{}\" and name in ({})" ) created_entities = [] + report_splitter = {"type": "label", "value": "---"} def __init__(self, session, plugins_presets={}): '''Expects a ftrack_api.Session instance''' From 59ca2f49754551017d81270c3ee6bb589c9800f7 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 Jun 2020 19:48:59 +0100 Subject: [PATCH 075/131] Hound --- pype/modules/ftrack/actions/action_batch_task_creation.py | 2 +- pype/modules/ftrack/lib/ftrack_base_handler.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pype/modules/ftrack/actions/action_batch_task_creation.py b/pype/modules/ftrack/actions/action_batch_task_creation.py index 89e2977aad..ef370d55eb 100644 --- a/pype/modules/ftrack/actions/action_batch_task_creation.py +++ b/pype/modules/ftrack/actions/action_batch_task_creation.py @@ -158,7 +158,7 @@ class BatchTasksAction(BaseAction): } -def register(session, plugins_presets={}): +def register(session, plugins_presets=None): '''Register action. Called when used as an event plugin.''' BatchTasksAction(session, plugins_presets).register() diff --git a/pype/modules/ftrack/lib/ftrack_base_handler.py b/pype/modules/ftrack/lib/ftrack_base_handler.py index c7144bb2f7..ce6607d6bf 100644 --- a/pype/modules/ftrack/lib/ftrack_base_handler.py +++ b/pype/modules/ftrack/lib/ftrack_base_handler.py @@ -36,7 +36,7 @@ class BaseHandler(object): ignore_me = False preactions = [] - def __init__(self, session, plugins_presets={}): + def __init__(self, session, plugins_presets=None): '''Expects a ftrack_api.Session instance''' self.log = Logger().get_logger(self.__class__.__name__) if not( @@ -57,6 +57,8 @@ class BaseHandler(object): # Using decorator self.register = self.register_decorator(self.register) self.launch = self.launch_log(self.launch) + if plugins_presets is None: + plugins_presets = {} self.plugins_presets = plugins_presets # Decorator From 46548eafa074350ce97eecc9e297e36878acddad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Jun 2020 13:58:06 +0200 Subject: [PATCH 076/131] toggle setData method happens on item not on model --- pype/tools/pyblish_pype/window.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 21ac500f9c..8f8c65e6ce 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -415,9 +415,9 @@ class Window(QtWidgets.QDialog): QtCore.Qt.DirectConnection ) - artist_view.toggled.connect(self.on_item_toggled) - overview_instance_view.toggled.connect(self.on_item_toggled) - overview_plugin_view.toggled.connect(self.on_item_toggled) + artist_view.toggled.connect(self.on_instance_toggle) + overview_instance_view.toggled.connect(self.on_instance_toggle) + overview_plugin_view.toggled.connect(self.on_plugin_toggle) footer_button_stop.clicked.connect(self.on_stop_clicked) footer_button_reset.clicked.connect(self.on_reset_clicked) @@ -537,7 +537,7 @@ class Window(QtWidgets.QDialog): ): instance_item.setData(enable_value, Roles.IsEnabledRole) - def on_item_toggled(self, index, state=None): + def on_instance_toggle(self, index, state=None): """An item is requesting to be toggled""" if not index.data(Roles.IsOptionalRole): return self.info("This item is mandatory") @@ -548,7 +548,27 @@ class Window(QtWidgets.QDialog): if state is None: state = not index.data(QtCore.Qt.CheckStateRole) - index.model().setData(index, state, QtCore.Qt.CheckStateRole) + instance_id = index.data(Roles.ObjectIdRole) + instanceitem = self.instance_model.instance_items[instance_id] + instanceitem.setData(state, QtCore.Qt.CheckStateRole) + + self.update_compatibility() + + def on_plugin_toggle(self, index, state=None): + """An item is requesting to be toggled""" + if not index.data(Roles.IsOptionalRole): + return self.info("This item is mandatory") + + if self.controller.collect_state != 1: + return self.info("Cannot toggle") + + if state is None: + state = not index.data(QtCore.Qt.CheckStateRole) + + plugin_id = index.data(Roles.ObjectIdRole) + plugin_item = self.plugin_model.plugin_items[plugin_id] + plugin_item.setData(state, QtCore.Qt.CheckStateRole) + self.update_compatibility() def on_tab_changed(self, target): From c69bf58d3bdc85a8b03a121419f0105f3d107412 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 16 Jun 2020 12:59:42 +0100 Subject: [PATCH 077/131] Fix publishing workfile - Unified template export across workfile and templates. - Fix scene saving per instance. - Cleanup template loader. --- pype/hosts/harmony/__init__.py | 45 +++++++++++++++ .../harmony/load/load_template_workfile.py | 25 --------- .../harmony/publish/extract_save_scene.py | 2 +- .../harmony/publish/extract_template.py | 55 +++---------------- .../harmony/publish/extract_workfile.py | 20 +++++-- 5 files changed, 70 insertions(+), 77 deletions(-) diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index cdb8b40777..4105759527 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -84,6 +84,51 @@ def ensure_scene_settings(): harmony.send({"function": func, "args": [valid_settings]}) +def export_template(backdrops, nodes, filepath): + func = """function func(args) + { + // Add an extra node just so a new group can be created. + var temp_node = node.add("Top", "temp_note", "NOTE", 0, 0, 0); + var template_group = node.createGroup(temp_node, "temp_group"); + node.deleteNode( template_group + "/temp_note" ); + + // This will make Node View to focus on the new group. + selection.clearSelection(); + selection.addNodeToSelection(template_group); + Action.perform("onActionEnterGroup()", "Node View"); + + // Recreate backdrops in group. + for (var i = 0 ; i < args[0].length; i++) + { + Backdrop.addBackdrop(template_group, args[0][i]); + }; + + // Copy-paste the selected nodes into the new group. + var drag_object = copyPaste.copy(args[1], 1, frame.numberOf, ""); + copyPaste.pasteNewNodes(drag_object, template_group, ""); + + // Select all nodes within group and export as template. + Action.perform( "selectAll()", "Node View" ); + copyPaste.createTemplateFromSelection(args[2], args[3]); + + // Unfocus the group in Node view, delete all nodes and backdrops + // created during the process. + Action.perform("onActionUpToParent()", "Node View"); + node.deleteNode(template_group, true, true); + } + func + """ + harmony.send({ + "function": func, + "args": [ + backdrops, + nodes, + os.path.basename(filepath), + os.path.dirname(filepath) + ] + }) + + def install(): print("Installing Pype config...") diff --git a/pype/plugins/harmony/load/load_template_workfile.py b/pype/plugins/harmony/load/load_template_workfile.py index 00d2e63c62..a9dcd0c776 100644 --- a/pype/plugins/harmony/load/load_template_workfile.py +++ b/pype/plugins/harmony/load/load_template_workfile.py @@ -14,18 +14,6 @@ class ImportTemplateLoader(api.Loader): label = "Import Template" def load(self, context, name=None, namespace=None, data=None): - # Make backdrops from metadata. - backdrops = context["representation"]["data"].get("backdrops", []) - - func = """function func(args) - { - Backdrop.addBackdrop("Top", args[0]); - } - func - """ - for backdrop in backdrops: - harmony.send({"function": func, "args": [backdrop]}) - # Import template. temp_dir = tempfile.mkdtemp() zip_file = api.get_representation_path(context["representation"]) @@ -33,19 +21,6 @@ class ImportTemplateLoader(api.Loader): with zipfile.ZipFile(zip_file, "r") as zip_ref: zip_ref.extractall(template_path) - func = """function func(args) - { - var template_path = args[0]; - var drag_object = copyPaste.copyFromTemplate( - template_path, 0, 0, copyPaste.getCurrentCreateOptions() - ); - copyPaste.pasteNewNodes( - drag_object, "", copyPaste.getCurrentPasteOptions() - ); - } - func - """ - func = """function func(args) { var template_path = args[0]; diff --git a/pype/plugins/harmony/publish/extract_save_scene.py b/pype/plugins/harmony/publish/extract_save_scene.py index 1733bdb95c..8b953580a7 100644 --- a/pype/plugins/harmony/publish/extract_save_scene.py +++ b/pype/plugins/harmony/publish/extract_save_scene.py @@ -9,5 +9,5 @@ class ExtractSaveScene(pyblish.api.ContextPlugin): order = pyblish.api.ExtractorOrder - 0.49 hosts = ["harmony"] - def process(self, instance): + def process(self, context): harmony.save_scene() diff --git a/pype/plugins/harmony/publish/extract_template.py b/pype/plugins/harmony/publish/extract_template.py index f7a5e34e67..1ba0befc54 100644 --- a/pype/plugins/harmony/publish/extract_template.py +++ b/pype/plugins/harmony/publish/extract_template.py @@ -2,7 +2,8 @@ import os import shutil import pype.api -from avalon import harmony +import avalon.harmony +import pype.hosts.harmony class ExtractTemplate(pype.api.Extractor): @@ -14,6 +15,7 @@ class ExtractTemplate(pype.api.Extractor): def process(self, instance): staging_dir = self.staging_dir(instance) + filepath = os.path.join(staging_dir, "{}.tpl".format(instance.name)) self.log.info("Outputting template to {}".format(staging_dir)) @@ -28,7 +30,7 @@ class ExtractTemplate(pype.api.Extractor): unique_backdrops = [backdrops[x] for x in set(backdrops.keys())] # Get non-connected nodes within backdrops. - all_nodes = harmony.send( + all_nodes = avalon.harmony.send( {"function": "node.subNodes", "args": ["Top"]} )["result"] for node in [x for x in all_nodes if x not in dependencies]: @@ -43,48 +45,9 @@ class ExtractTemplate(pype.api.Extractor): dependencies.remove(instance[0]) # Export template. - func = """function func(args) - { - // Add an extra node just so a new group can be created. - var temp_node = node.add("Top", "temp_note", "NOTE", 0, 0, 0); - var template_group = node.createGroup(temp_node, "temp_group"); - node.deleteNode( template_group + "/temp_note" ); - - // This will make Node View to focus on the new group. - selection.clearSelection(); - selection.addNodeToSelection(template_group); - Action.perform("onActionEnterGroup()", "Node View"); - - // Recreate backdrops in group. - for (var i = 0 ; i < args[0].length; i++) - { - Backdrop.addBackdrop(template_group, args[0][i]); - }; - - // Copy-paste the selected nodes into the new group. - var drag_object = copyPaste.copy(args[1], 1, frame.numberOf, ""); - copyPaste.pasteNewNodes(drag_object, template_group, ""); - - // Select all nodes within group and export as template. - Action.perform( "selectAll()", "Node View" ); - copyPaste.createTemplateFromSelection(args[2], args[3]); - - // Unfocus the group in Node view, delete all nodes and backdrops - // created during the process. - Action.perform("onActionUpToParent()", "Node View"); - node.deleteNode(template_group, true, true); - } - func - """ - harmony.send({ - "function": func, - "args": [ - unique_backdrops, - dependencies, - "{}.tpl".format(instance.name), - staging_dir - ] - }) + pype.hosts.harmony.export_template( + unique_backdrops, dependencies, filepath + ) # Prep representation. os.chdir(staging_dir) @@ -131,7 +94,7 @@ class ExtractTemplate(pype.api.Extractor): } func """ - return harmony.send( + return avalon.harmony.send( {"function": func, "args": [node]} )["result"] @@ -150,7 +113,7 @@ class ExtractTemplate(pype.api.Extractor): func """ - current_dependencies = harmony.send( + current_dependencies = avalon.harmony.send( {"function": func, "args": [node]} )["result"] diff --git a/pype/plugins/harmony/publish/extract_workfile.py b/pype/plugins/harmony/publish/extract_workfile.py index 7a0a7954dd..304b70e293 100644 --- a/pype/plugins/harmony/publish/extract_workfile.py +++ b/pype/plugins/harmony/publish/extract_workfile.py @@ -2,6 +2,8 @@ import os import shutil import pype.api +import avalon.harmony +import pype.hosts.harmony class ExtractWorkfile(pype.api.Extractor): @@ -12,17 +14,25 @@ class ExtractWorkfile(pype.api.Extractor): families = ["workfile"] def process(self, instance): - file_path = instance.context.data["currentFile"] + # Export template. + backdrops = avalon.harmony.send( + {"function": "Backdrop.backdrops", "args": ["Top"]} + )["result"] + nodes = avalon.harmony.send( + {"function": "node.subNodes", "args": ["Top"]} + )["result"] staging_dir = self.staging_dir(instance) + filepath = os.path.join(staging_dir, "{}.tpl".format(instance.name)) + pype.hosts.harmony.export_template(backdrops, nodes, filepath) + + # Prep representation. os.chdir(staging_dir) shutil.make_archive( - instance.name, + "{}".format(instance.name), "zip", - os.path.dirname(file_path) + os.path.join(staging_dir, "{}.tpl".format(instance.name)) ) - zip_path = os.path.join(staging_dir, instance.name + ".zip") - self.log.info(f"Output zip file: {zip_path}") representation = { "name": "tpl", From 65735c3d462df53de15ca0671591522d14dfc43a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 16 Jun 2020 13:02:34 +0100 Subject: [PATCH 078/131] Fix extract render Was not making a review movie for Ftrack. --- .../plugins/harmony/publish/extract_render.py | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/pype/plugins/harmony/publish/extract_render.py b/pype/plugins/harmony/publish/extract_render.py index bd98cd5365..75d0d2ae36 100644 --- a/pype/plugins/harmony/publish/extract_render.py +++ b/pype/plugins/harmony/publish/extract_render.py @@ -82,6 +82,7 @@ class ExtractRender(pyblish.api.InstancePlugin): path, len(collections) ) ) + collection = collections[0] # Generate thumbnail. thumbnail_path = os.path.join(path, "thumbnail.png") @@ -106,12 +107,40 @@ class ExtractRender(pyblish.api.InstancePlugin): self.log.debug(output.decode("utf-8")) + # Generate mov. + mov_path = os.path.join(path, instance.data["name"] + ".mov") + args = [ + "ffmpeg", "-y", + "-i", + os.path.join(path, collection.head + "%04d" + collection.tail), + mov_path + ] + process = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.PIPE + ) + + output = process.communicate()[0] + + if process.returncode != 0: + raise ValueError(output.decode("utf-8")) + + self.log.debug(output.decode("utf-8")) + # Generate representations. - extension = os.path.splitext(list(collections[0])[0])[-1][1:] + extension = collection.tail[1:] representation = { "name": extension, "ext": extension, - "files": list(collections[0]), + "files": list(collection), + "stagingDir": path + } + movie = { + "name": "mov", + "ext": "mov", + "files": os.path.basename(mov_path), "stagingDir": path, "frameStart": frame_start, "frameEnd": frame_end, @@ -126,7 +155,7 @@ class ExtractRender(pyblish.api.InstancePlugin): "stagingDir": path, "tags": ["thumbnail"] } - instance.data["representations"] = [representation, thumbnail] + instance.data["representations"] = [representation, movie, thumbnail] # Required for extract_review plugin (L222 onwards). instance.data["frameStart"] = frame_start From 2c398c07ba3e479900579a06f994b7b376ca7608 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 16 Jun 2020 15:06:57 +0100 Subject: [PATCH 079/131] Validate scene settings. --- pype/hosts/harmony/__init__.py | 90 ++++++++++--------- .../publish/validate_scene_settings.py | 60 +++++++++++++ 2 files changed, 109 insertions(+), 41 deletions(-) create mode 100644 pype/plugins/harmony/publish/validate_scene_settings.py diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index cdb8b40777..169786204e 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -7,46 +7,7 @@ import pyblish.api from pype import lib -def ensure_scene_settings(): - asset_data = lib.get_asset()["data"] - fps = asset_data.get("fps") - frame_start = asset_data.get("frameStart") - frame_end = asset_data.get("frameEnd") - resolution_width = asset_data.get("resolutionWidth") - resolution_height = asset_data.get("resolutionHeight") - - settings = { - "fps": fps, - "frameStart": frame_start, - "frameEnd": frame_end, - "resolutionWidth": resolution_width, - "resolutionHeight": resolution_height - } - - invalid_settings = [] - valid_settings = {} - for key, value in settings.items(): - if value is None: - invalid_settings.append(key) - else: - valid_settings[key] = value - - # Warn about missing attributes. - print("Starting new QApplication..") - app = Qt.QtWidgets.QApplication(sys.argv) - - message_box = Qt.QtWidgets.QMessageBox() - message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning) - msg = "Missing attributes:" - if invalid_settings: - for item in invalid_settings: - msg += f"\n{item}" - message_box.setText(msg) - message_box.exec_() - - # Garbage collect QApplication. - del app - +def set_scene_settings(settings): func = """function func(args) { if (args[0]["fps"]) @@ -81,7 +42,54 @@ def ensure_scene_settings(): } func """ - harmony.send({"function": func, "args": [valid_settings]}) + harmony.send({"function": func, "args": [settings]}) + + +def get_asset_settings(): + asset_data = lib.get_asset()["data"] + fps = asset_data.get("fps") + frame_start = asset_data.get("frameStart") + frame_end = asset_data.get("frameEnd") + resolution_width = asset_data.get("resolutionWidth") + resolution_height = asset_data.get("resolutionHeight") + + return { + "fps": fps, + "frameStart": frame_start, + "frameEnd": frame_end, + "resolutionWidth": resolution_width, + "resolutionHeight": resolution_height + } + + +def ensure_scene_settings(): + settings = get_asset_settings() + + invalid_settings = [] + valid_settings = {} + for key, value in settings.items(): + if value is None: + invalid_settings.append(key) + else: + valid_settings[key] = value + + # Warn about missing attributes. + print("Starting new QApplication..") + app = Qt.QtWidgets.QApplication(sys.argv) + + message_box = Qt.QtWidgets.QMessageBox() + message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning) + msg = "Missing attributes:" + if invalid_settings: + for item in invalid_settings: + msg += f"\n{item}" + message_box.setText(msg) + message_box.exec_() + + # Garbage collect QApplication. + del app + + set_scene_settings(valid_settings) def install(): diff --git a/pype/plugins/harmony/publish/validate_scene_settings.py b/pype/plugins/harmony/publish/validate_scene_settings.py new file mode 100644 index 0000000000..260d64c42b --- /dev/null +++ b/pype/plugins/harmony/publish/validate_scene_settings.py @@ -0,0 +1,60 @@ +import json + +import pyblish.api + +import avalon.harmony +import pype.hosts.harmony + + +class ValidateSceneSettingsRepair(pyblish.api.Action): + """Repair the instance.""" + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + pype.hosts.harmony.set_scene_settings( + pype.hosts.harmony.get_asset_settings() + ) + + +class ValidateSceneSettings(pyblish.api.InstancePlugin): + """Ensure the scene settings are in sync with database.""" + + order = pyblish.api.ValidatorOrder + label = "Validate Scene Settings" + families = ["workfile"] + hosts = ["harmony"] + actions = [ValidateSceneSettingsRepair] + + def process(self, instance): + expected_settings = pype.hosts.harmony.get_asset_settings() + + func = """function func() + { + return { + "fps": scene.getFrameRate(), + "frameStart": scene.getStartFrame(), + "frameEnd": scene.getStopFrame(), + "resolutionWidth": scene.defaultResolutionX(), + "resolutionHeight": scene.defaultResolutionY() + }; + } + func + """ + current_settings = avalon.harmony.send({"function": func})["result"] + + invalid_settings = [] + for key, value in expected_settings.items(): + if value != current_settings[key]: + invalid_settings.append({ + "name": key, + "expected": value, + "current": current_settings[key] + }) + + msg = "Found invalid settings:\n{}".format( + json.dumps(invalid_settings, sort_keys=True, indent=4) + ) + assert not invalid_settings, msg From 9042e606cf070a8ae24ddbd47a38fac32063d6de Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 16 Jun 2020 15:26:11 +0100 Subject: [PATCH 080/131] Load imagesequence. --- .../harmony/load/load_imagesequence.py | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 pype/plugins/harmony/load/load_imagesequence.py diff --git a/pype/plugins/harmony/load/load_imagesequence.py b/pype/plugins/harmony/load/load_imagesequence.py new file mode 100644 index 0000000000..cdb6d5a6b8 --- /dev/null +++ b/pype/plugins/harmony/load/load_imagesequence.py @@ -0,0 +1,228 @@ +import os + +import clique + +from avalon import api, harmony, io + +copy_files = """function copyFile(srcFilename, dstFilename) +{ + var srcFile = new PermanentFile(srcFilename); + var dstFile = new PermanentFile(dstFilename); + srcFile.copy(dstFile); +} +""" + +import_files = """var PNGTransparencyMode = 0; //Premultiplied wih Black +var TGATransparencyMode = 0; //Premultiplied wih Black +var SGITransparencyMode = 0; //Premultiplied wih Black +var LayeredPSDTransparencyMode = 1; //Straight +var FlatPSDTransparencyMode = 2; //Premultiplied wih White + +function getUniqueColumnName( column_prefix ) +{ + var suffix = 0; + // finds if unique name for a column + var column_name = column_prefix; + while(suffix < 2000) + { + if(!column.type(column_name)) + break; + + suffix = suffix + 1; + column_name = column_prefix + "_" + suffix; + } + return column_name; +} + +function import_files(args) +{ + var root = args[0]; + var files = args[1]; + var name = args[2]; + var start_frame = args[3]; + + var vectorFormat = null; + var extension = null; + var filename = files[0]; + + var pos = filename.lastIndexOf("."); + if( pos < 0 ) + return null; + + extension = filename.substr(pos+1).toLowerCase(); + + if(extension == "jpeg") + extension = "jpg"; + if(extension == "tvg") + { + vectorFormat = "TVG" + extension ="SCAN"; // element.add() will use this. + } + + var elemId = element.add( + name, + "BW", + scene.numberOfUnitsZ(), + extension.toUpperCase(), + vectorFormat + ); + if (elemId == -1) + { + // hum, unknown file type most likely -- let's skip it. + return null; // no read to add. + } + + var uniqueColumnName = getUniqueColumnName(name); + column.add(uniqueColumnName , "DRAWING"); + column.setElementIdOfDrawing(uniqueColumnName, elemId); + + var read = node.add(root, name, "READ", 0, 0, 0); + var transparencyAttr = node.getAttr( + read, frame.current(), "READ_TRANSPARENCY" + ); + var opacityAttr = node.getAttr(read, frame.current(), "OPACITY"); + transparencyAttr.setValue(true); + opacityAttr.setValue(true); + + var alignmentAttr = node.getAttr(read, frame.current(), "ALIGNMENT_RULE"); + alignmentAttr.setValue("ASIS"); + + var transparencyModeAttr = node.getAttr( + read, frame.current(), "applyMatteToColor" + ); + if (extension == "png") + transparencyModeAttr.setValue(PNGTransparencyMode); + if (extension == "tga") + transparencyModeAttr.setValue(TGATransparencyMode); + if (extension == "sgi") + transparencyModeAttr.setValue(SGITransparencyMode); + if (extension == "psd") + transparencyModeAttr.setValue(FlatPSDTransparencyMode); + + node.linkAttr(read, "DRAWING.ELEMENT", uniqueColumnName); + + // Create a drawing for each file. + for( var i =0; i <= files.length - 1; ++i) + { + timing = start_frame + i + // Create a drawing drawing, 'true' indicate that the file exists. + Drawing.create(elemId, timing, true); + // Get the actual path, in tmp folder. + var drawingFilePath = Drawing.filename(elemId, timing.toString()); + copyFile( files[i], drawingFilePath ); + + column.setEntry(uniqueColumnName, 1, timing, timing.toString()); + } + return read; +} +import_files +""" + +replace_files = """function replace_files(args) +{ + var files = args[0]; + var _node = args[1]; + var start_frame = args[2]; + + var _column = node.linkedColumn(_node, "DRAWING.ELEMENT"); + + // Delete existing drawings. + var timings = column.getDrawingTimings(_column); + for( var i =0; i <= timings.length - 1; ++i) + { + column.deleteDrawingAt(_column, parseInt(timings[i])); + } + + // Create new drawings. + for( var i =0; i <= files.length - 1; ++i) + { + timing = start_frame + i + // Create a drawing drawing, 'true' indicate that the file exists. + Drawing.create(node.getElementId(_node), timing, true); + // Get the actual path, in tmp folder. + var drawingFilePath = Drawing.filename( + node.getElementId(_node), timing.toString() + ); + copyFile( files[i], drawingFilePath ); + + column.setEntry(_column, 1, timing, timing.toString()); + } +} +replace_files +""" + + +class ImageSequenceLoader(api.Loader): + """Load images + Stores the imported asset in a container named after the asset. + """ + families = ["shot", "render"] + representations = ["jpeg", "png"] + + def load(self, context, name=None, namespace=None, data=None): + + collections, remainder = clique.assemble( + os.listdir(os.path.dirname(self.fname)) + ) + files = [] + for f in list(collections[0]): + files.append( + os.path.join(os.path.dirname(self.fname), f).replace("\\", "/") + ) + + read_node = harmony.send( + { + "function": copy_files + import_files, + "args": ["Top", files, context["subset"]["name"], 1] + } + )["result"] + + self[:] = [read_node] + + return harmony.containerise( + name, + namespace, + read_node, + context, + self.__class__.__name__ + ) + + def update(self, container, representation): + node = container.pop("node") + + collections, remainder = clique.assemble( + os.listdir( + os.path.dirname(api.get_representation_path(representation)) + ) + ) + files = [] + for f in list(collections[0]): + files.append( + os.path.join(os.path.dirname(self.fname), f).replace("\\", "/") + ) + + harmony.send( + { + "function": copy_files + replace_files, + "args": [files, node, 1] + } + ) + + harmony.imprint( + node, {"representation": str(representation["_id"])} + ) + + def remove(self, container): + node = container.pop("node") + func = """function deleteNode(_node) + { + node.deleteNode(_node, true, true); + } + deleteNode + """ + harmony.send( + {"function": func, "args": [node]} + ) + + def switch(self, container, representation): + self.update(container, representation) From 7b0c22ab5fe979b96c5b35c7b21f889e89a06c15 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 16 Jun 2020 15:27:58 +0100 Subject: [PATCH 081/131] Hound --- pype/plugins/harmony/load/load_imagesequence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/harmony/load/load_imagesequence.py b/pype/plugins/harmony/load/load_imagesequence.py index cdb6d5a6b8..7862e027af 100644 --- a/pype/plugins/harmony/load/load_imagesequence.py +++ b/pype/plugins/harmony/load/load_imagesequence.py @@ -2,7 +2,7 @@ import os import clique -from avalon import api, harmony, io +from avalon import api, harmony copy_files = """function copyFile(srcFilename, dstFilename) { From b210e16c90acbdbf005c953c51bd0f0f33a8565a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Jun 2020 16:55:25 +0200 Subject: [PATCH 082/131] widgets for log details are not set automatically for each log --- pype/tools/pyblish_pype/model.py | 2 -- pype/tools/pyblish_pype/widgets.py | 5 ----- pype/tools/pyblish_pype/window.py | 5 ----- 3 files changed, 12 deletions(-) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index 2c2661b5ec..91c6c595eb 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -1053,7 +1053,6 @@ class TerminalModel(QtGui.QStandardItemModel): self.reset() def reset(self): - self.items_to_set_widget = queue.Queue() self.clear() def prepare_records(self, result): @@ -1144,7 +1143,6 @@ class TerminalModel(QtGui.QStandardItemModel): detail_item = QtGui.QStandardItem(detail_text) detail_item.setData(TerminalDetailType, Roles.TypeRole) top_item.appendRow(detail_item) - self.items_to_set_widget.put(detail_item) def update_with_result(self, result): for record in result["records"]: diff --git a/pype/tools/pyblish_pype/widgets.py b/pype/tools/pyblish_pype/widgets.py index e81633f7a3..880d4755ad 100644 --- a/pype/tools/pyblish_pype/widgets.py +++ b/pype/tools/pyblish_pype/widgets.py @@ -321,11 +321,6 @@ class PerspectiveWidget(QtWidgets.QWidget): data = {"records": records} self.terminal_model.reset() self.terminal_model.update_with_result(data) - while not self.terminal_model.items_to_set_widget.empty(): - item = self.terminal_model.items_to_set_widget.get() - widget = TerminalDetail(item.data(QtCore.Qt.DisplayRole)) - index = self.terminal_proxy.mapFromSource(item.index()) - self.terminal_view.setIndexWidget(index, widget) self.records.button_toggle_text.setText( "{} ({})".format(self.l_rec, len_records) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 21ac500f9c..3b70caa7c5 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -815,11 +815,6 @@ class Window(QtWidgets.QDialog): instance_item = self.instance_model.update_with_result(result) self.terminal_model.update_with_result(result) - while not self.terminal_model.items_to_set_widget.empty(): - item = self.terminal_model.items_to_set_widget.get() - widget = widgets.TerminalDetail(item.data(QtCore.Qt.DisplayRole)) - index = self.terminal_proxy.mapFromSource(item.index()) - self.terminal_view.setIndexWidget(index, widget) self.update_compatibility() From bc20ac38fd91ff578fdb214e0e6b541320a9fc71 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Jun 2020 16:55:54 +0200 Subject: [PATCH 083/131] widget for log detail is set before expanding --- pype/tools/pyblish_pype/view.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pype/tools/pyblish_pype/view.py b/pype/tools/pyblish_pype/view.py index ada19bc7d9..32d9bcb04d 100644 --- a/pype/tools/pyblish_pype/view.py +++ b/pype/tools/pyblish_pype/view.py @@ -1,5 +1,6 @@ from Qt import QtCore, QtWidgets from . import model +from . import widgets from .constants import Roles @@ -190,6 +191,20 @@ class TerminalView(QtWidgets.QTreeView): self.updateGeometry() self.scrollToBottom() + def expand(self, index): + """Wrapper to set widget for expanded index.""" + model = index.model() + row_count = model.rowCount(index) + for child_idx in range(row_count): + child_index = model.index(child_idx, index.column(), index) + widget = self.indexWidget(child_index) + if widget is None: + widget = widgets.TerminalDetail( + child_index.data(QtCore.Qt.DisplayRole) + ) + self.setIndexWidget(child_index, widget) + super(TerminalView, self).expand(index) + def resizeEvent(self, event): super(self.__class__, self).resizeEvent(event) self.model().layoutChanged.emit() From c49b83ac980ef4f8d9a1f55e361418dbcb34581e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Jun 2020 17:01:58 +0200 Subject: [PATCH 084/131] stop publishing in middle of instance loop --- pype/tools/pyblish_pype/control.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pype/tools/pyblish_pype/control.py b/pype/tools/pyblish_pype/control.py index c454e4c6fa..12c8944642 100644 --- a/pype/tools/pyblish_pype/control.py +++ b/pype/tools/pyblish_pype/control.py @@ -302,6 +302,11 @@ class Controller(QtCore.QObject): "%s was inactive, skipping.." % instance ) continue + # Stop if was stopped + if self.stopped: + self.stopped = False + yield IterationBreak("Stopped") + yield (plugin, instance) else: families = util.collect_families_from_instances( From 086059cbf3519167cc165a099899d64eb72d689e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Jun 2020 17:47:03 +0200 Subject: [PATCH 085/131] controller can handle instance toggle callbacks --- pype/tools/pyblish_pype/control.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pype/tools/pyblish_pype/control.py b/pype/tools/pyblish_pype/control.py index c454e4c6fa..21cf660e63 100644 --- a/pype/tools/pyblish_pype/control.py +++ b/pype/tools/pyblish_pype/control.py @@ -8,6 +8,7 @@ an active window manager; such as via Travis-CI. import os import sys import traceback +import inspect from Qt import QtCore @@ -60,11 +61,15 @@ class Controller(QtCore.QObject): # store OrderGroups - now it is a singleton order_groups = util.OrderGroups + # When instance is toggled + instance_toggled = QtCore.Signal(object, object, object) + def __init__(self, parent=None): super(Controller, self).__init__(parent) self.context = None self.plugins = {} self.optional_default = {} + self.instance_toggled.connect(self._on_instance_toggled) def reset_variables(self): # Data internal to the GUI itself @@ -410,3 +415,19 @@ class Controller(QtCore.QObject): for plugin in self.plugins: del(plugin) + + def _on_instance_toggled(self, instance, new_value, old_value): + callbacks = pyblish.api.registered_callbacks().get("instanceToggled") + if not callbacks: + return + + for callback in callbacks: + try: + callback(instance, new_value, old_value) + except Exception: + print( + "Callback for `instanceToggled` crashed. {}".format( + os.path.abspath(inspect.getfile(callback)) + ) + ) + traceback.print_exception(*sys.exc_info()) From 805ca009913bed2e163b365d597e0cf5171141e0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Jun 2020 17:47:22 +0200 Subject: [PATCH 086/131] pyblish gui trigger callbacks on instance toggle --- pype/tools/pyblish_pype/window.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 8f8c65e6ce..0c914a0c4b 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -545,12 +545,17 @@ class Window(QtWidgets.QDialog): if self.controller.collect_state != 1: return self.info("Cannot toggle") + current_state = index.data(QtCore.Qt.CheckStateRole) if state is None: - state = not index.data(QtCore.Qt.CheckStateRole) + state = not current_state instance_id = index.data(Roles.ObjectIdRole) - instanceitem = self.instance_model.instance_items[instance_id] - instanceitem.setData(state, QtCore.Qt.CheckStateRole) + instance_item = self.instance_model.instance_items[instance_id] + instance_item.setData(state, QtCore.Qt.CheckStateRole) + + self.controller.instance_toggled.emit( + instance_item.instance, state, current_state + ) self.update_compatibility() From e547967e1be58091aedc3fff4d4a6f3d1709f205 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Jun 2020 18:47:57 +0200 Subject: [PATCH 087/131] reversed instance_toggled new/old value --- pype/tools/pyblish_pype/control.py | 4 ++-- pype/tools/pyblish_pype/window.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/tools/pyblish_pype/control.py b/pype/tools/pyblish_pype/control.py index 21cf660e63..aa39663182 100644 --- a/pype/tools/pyblish_pype/control.py +++ b/pype/tools/pyblish_pype/control.py @@ -416,14 +416,14 @@ class Controller(QtCore.QObject): for plugin in self.plugins: del(plugin) - def _on_instance_toggled(self, instance, new_value, old_value): + def _on_instance_toggled(self, instance, old_value, new_value): callbacks = pyblish.api.registered_callbacks().get("instanceToggled") if not callbacks: return for callback in callbacks: try: - callback(instance, new_value, old_value) + callback(instance, old_value, new_value) except Exception: print( "Callback for `instanceToggled` crashed. {}".format( diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 0c914a0c4b..5d22e5ac8f 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -554,7 +554,7 @@ class Window(QtWidgets.QDialog): instance_item.setData(state, QtCore.Qt.CheckStateRole) self.controller.instance_toggled.emit( - instance_item.instance, state, current_state + instance_item.instance, current_state, state ) self.update_compatibility() From 75366a3a3b2e5c6dd458f5517b13a6c197cdd813 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Jun 2020 11:07:12 +0200 Subject: [PATCH 088/131] log detail is calculated after expanding --- pype/tools/pyblish_pype/model.py | 90 +++++++++++++++++++------------- pype/tools/pyblish_pype/view.py | 9 ++-- 2 files changed, 59 insertions(+), 40 deletions(-) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index 91c6c595eb..6b8aaeb79e 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -32,7 +32,6 @@ from .awesome import tags as awesome import Qt from Qt import QtCore, QtGui from six import text_type -from six.moves import queue from .vendor import qtawesome from .constants import PluginStates, InstanceStates, GroupStates, Roles @@ -1009,7 +1008,7 @@ class ArtistProxy(QtCore.QAbstractProxyModel): return QtCore.QModelIndex() -class TerminalModel(QtGui.QStandardItemModel): +class TerminalDetailItem(QtGui.QStandardItem): key_label_record_map = ( ("instance", "Instance"), ("msg", "Message"), @@ -1022,6 +1021,57 @@ class TerminalModel(QtGui.QStandardItemModel): ("msecs", "Millis") ) + def __init__(self, record_item): + self.record_item = record_item + self.msg = None + msg = record_item.get("msg") + if msg is None: + msg = record_item["label"].split("\n")[0] + + super(TerminalDetailItem, self).__init__(msg) + + def data(self, role=QtCore.Qt.DisplayRole): + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + if self.msg is None: + self.msg = self.compute_detail_text(self.record_item) + return self.msg + return super(TerminalDetailItem, self).data(role) + + def compute_detail_text(self, item_data): + if item_data["type"] == "info": + return item_data["label"] + + html_text = "" + for key, title in self.key_label_record_map: + if key not in item_data: + continue + value = item_data[key] + text = ( + str(value) + .replace("<", "<") + .replace(">", ">") + .replace('\n', '
') + .replace(' ', ' ') + ) + + title_tag = ( + '{}: ' + ' color:#fff;\" >{}: ' + ).format(title) + + html_text += ( + '{}' + '{}' + ).format(title_tag, text) + + html_text = '{}
'.format( + html_text + ) + return html_text + + +class TerminalModel(QtGui.QStandardItemModel): item_icon_name = { "info": "fa.info", "record": "fa.circle", @@ -1139,8 +1189,7 @@ class TerminalModel(QtGui.QStandardItemModel): self.appendRow(top_item) - detail_text = self.prepare_detail_text(record_item) - detail_item = QtGui.QStandardItem(detail_text) + detail_item = TerminalDetailItem(record_item) detail_item.setData(TerminalDetailType, Roles.TypeRole) top_item.appendRow(detail_item) @@ -1148,39 +1197,6 @@ class TerminalModel(QtGui.QStandardItemModel): for record in result["records"]: self.append(record) - def prepare_detail_text(self, item_data): - if item_data["type"] == "info": - return item_data["label"] - - html_text = "" - for key, title in self.key_label_record_map: - if key not in item_data: - continue - value = item_data[key] - text = ( - str(value) - .replace("<", "<") - .replace(">", ">") - .replace('\n', '
') - .replace(' ', ' ') - ) - - title_tag = ( - '{}: ' - ' color:#fff;\" >{}: ' - ).format(title) - - html_text += ( - '{}' - '{}' - ).format(title_tag, text) - - html_text = '{}
'.format( - html_text - ) - return html_text - class TerminalProxy(QtCore.QSortFilterProxyModel): filter_buttons_checks = { diff --git a/pype/tools/pyblish_pype/view.py b/pype/tools/pyblish_pype/view.py index 32d9bcb04d..03509604bb 100644 --- a/pype/tools/pyblish_pype/view.py +++ b/pype/tools/pyblish_pype/view.py @@ -195,15 +195,18 @@ class TerminalView(QtWidgets.QTreeView): """Wrapper to set widget for expanded index.""" model = index.model() row_count = model.rowCount(index) + is_new = False for child_idx in range(row_count): child_index = model.index(child_idx, index.column(), index) widget = self.indexWidget(child_index) if widget is None: - widget = widgets.TerminalDetail( - child_index.data(QtCore.Qt.DisplayRole) - ) + is_new = True + msg = child_index.data(QtCore.Qt.DisplayRole) + widget = widgets.TerminalDetail(msg) self.setIndexWidget(child_index, widget) super(TerminalView, self).expand(index) + if is_new: + self.updateGeometries() def resizeEvent(self, event): super(self.__class__, self).resizeEvent(event) From b1bfe8a39d655b2c6a9c3092e9e9eb38a2d1b0ae Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Jun 2020 11:16:07 +0200 Subject: [PATCH 089/131] formatting cleanup --- pype/tools/pyblish_pype/model.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index 6b8aaeb79e..e316d5781a 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -48,6 +48,7 @@ TerminalDetailType = QtGui.QStandardItem.UserType + 4 class QAwesomeTextIconFactory: icons = {} + @classmethod def icon(cls, icon_name): if icon_name not in cls.icons: @@ -57,6 +58,7 @@ class QAwesomeTextIconFactory: class QAwesomeIconFactory: icons = {} + @classmethod def icon(cls, icon_name, icon_color): if icon_name not in cls.icons: From 13144b18cf4f286c2b20b6c0b871fdfcbb3e5e84 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 17 Jun 2020 10:25:46 +0100 Subject: [PATCH 090/131] Optimize processing Ffmpeg validator is now a context plugin instead of instance. --- pype/plugins/global/publish/validate_ffmpeg_installed.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/validate_ffmpeg_installed.py b/pype/plugins/global/publish/validate_ffmpeg_installed.py index f6738e6de1..61127ff96e 100644 --- a/pype/plugins/global/publish/validate_ffmpeg_installed.py +++ b/pype/plugins/global/publish/validate_ffmpeg_installed.py @@ -8,12 +8,11 @@ except ImportError: import errno -class ValidateFFmpegInstalled(pyblish.api.Validator): +class ValidateFFmpegInstalled(pyblish.api.ContextPlugin): """Validate availability of ffmpeg tool in PATH""" order = pyblish.api.ValidatorOrder label = 'Validate ffmpeg installation' - families = ['review'] optional = True def is_tool(self, name): @@ -27,7 +26,7 @@ class ValidateFFmpegInstalled(pyblish.api.Validator): return False return True - def process(self, instance): + def process(self, context): ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") self.log.info("ffmpeg path: `{}`".format(ffmpeg_path)) if self.is_tool(ffmpeg_path) is False: From 8f722095b078b3baa53f802bb102187897f9a414 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 17 Jun 2020 10:28:27 +0100 Subject: [PATCH 091/131] Validate shots For duplicate shot names. --- .../publish/validate_shots.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 pype/plugins/standalonepublisher/publish/validate_shots.py diff --git a/pype/plugins/standalonepublisher/publish/validate_shots.py b/pype/plugins/standalonepublisher/publish/validate_shots.py new file mode 100644 index 0000000000..3267af7685 --- /dev/null +++ b/pype/plugins/standalonepublisher/publish/validate_shots.py @@ -0,0 +1,23 @@ +import pyblish.api +import pype.api + + +class ValidateShots(pyblish.api.ContextPlugin): + """Validate there is a "mov" next to the editorial file.""" + + label = "Validate Shots" + hosts = ["standalonepublisher"] + order = pype.api.ValidateContentsOrder + + def process(self, context): + shot_names = [] + duplicate_names = [] + for instance in context: + name = instance.data["name"] + if name in shot_names: + duplicate_names.append(name) + else: + shot_names.append(name) + + msg = "There are duplicate shot names:\n{}".format(duplicate_names) + assert not duplicate_names, msg From a88bae8b4b205c64018f46463f331fb9d787b3dd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 17 Jun 2020 10:34:19 +0100 Subject: [PATCH 092/131] Collect shots Shot names are collected from the editorial file instead of sequencial numbering. --- .../standalonepublisher/publish/collect_shots.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_shots.py b/pype/plugins/standalonepublisher/publish/collect_shots.py index 80ee875add..853ba4e8de 100644 --- a/pype/plugins/standalonepublisher/publish/collect_shots.py +++ b/pype/plugins/standalonepublisher/publish/collect_shots.py @@ -60,14 +60,8 @@ class CollectShots(pyblish.api.InstancePlugin): # options to be more flexible. asset_name = asset_name.split("_")[0] - shot_number = 10 + instances = [] for track in tracks: - self.log.info(track) - - if "audio" in track.name.lower(): - continue - - instances = [] for child in track.each_child(): # Transitions are ignored, because Clips have the full frame @@ -75,10 +69,13 @@ class CollectShots(pyblish.api.InstancePlugin): if isinstance(child, otio.schema.transition.Transition): continue + # Hardcoded to expect a shot name of "[name].[extension]" + child_name = os.path.splitext(child.name)[0].lower() + name = f"{asset_name}_{child_name}" + frame_start = child.range_in_parent().start_time.value frame_end = child.range_in_parent().end_time_inclusive().value - name = f"{asset_name}_sh{shot_number:04}" label = f"{name} (framerange: {frame_start}-{frame_end})" instances.append( instance.context.create_instance(**{ @@ -96,8 +93,6 @@ class CollectShots(pyblish.api.InstancePlugin): }) ) - shot_number += 10 - visual_hierarchy = [asset_entity] while True: visual_parent = io.find_one( From 4be7c102cf855897f96e73dad6f045aa1af41e59 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 17 Jun 2020 11:45:56 +0100 Subject: [PATCH 093/131] Install pywin requirement with prehook --- pype/hooks/photoshop/prelaunch.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 pype/hooks/photoshop/prelaunch.py diff --git a/pype/hooks/photoshop/prelaunch.py b/pype/hooks/photoshop/prelaunch.py new file mode 100644 index 0000000000..4f00e4cd83 --- /dev/null +++ b/pype/hooks/photoshop/prelaunch.py @@ -0,0 +1,23 @@ +import pype.lib +from pype.api import Logger + + +class PhotoshopPrelaunch(pype.lib.PypeHook): + """This hook will check for the existence of PyWin + + PyWin is a requirement for the Photoshop integration. + """ + project_code = None + + def __init__(self, logger=None): + if not logger: + self.log = Logger().get_logger(self.__class__.__name__) + else: + self.log = logger + + self.signature = "( {} )".format(self.__class__.__name__) + + def execute(self, *args, env: dict = None) -> bool: + output = pype.lib._subprocess(["pip", "install", "pywin32==227"]) + self.log.info(output) + return True From d3794478a950f353da9ac04478fca1aff4ea8ba4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Jun 2020 15:20:44 +0200 Subject: [PATCH 094/131] play, validate buttons are not available on reset but on stop --- pype/tools/pyblish_pype/window.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 5d22e5ac8f..a6e6cd245e 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -726,14 +726,12 @@ class Window(QtWidgets.QDialog): self.on_tab_changed(self.state["current_page"]) self.update_compatibility() - self.footer_button_validate.setEnabled(True) - self.footer_button_reset.setEnabled(True) - self.footer_button_stop.setEnabled(False) - self.footer_button_play.setEnabled(True) - self.footer_button_play.setFocus() + self.footer_button_validate.setEnabled(False) + self.footer_button_reset.setEnabled(False) + self.footer_button_stop.setEnabled(True) + self.footer_button_play.setEnabled(False) def on_passed_group(self, order): - for group_item in self.instance_model.group_items.values(): if self.overview_instance_view.isExpanded(group_item.index()): continue @@ -764,11 +762,17 @@ class Window(QtWidgets.QDialog): ) def on_was_stopped(self): - errored = self.controller.errored - self.footer_button_play.setEnabled(not errored) - self.footer_button_validate.setEnabled( - not errored and not self.controller.validated - ) + if self.controller.collected: + errored = self.controller.errored + self.footer_button_play.setEnabled(not errored) + self.footer_button_validate.setEnabled( + not errored and not self.controller.validated + ) + else: + self.footer_button_play.setEnabled(False) + self.footer_button_validate.setEnabled(False) + self.footer_button_play.setFocus() + self.footer_button_reset.setEnabled(True) self.footer_button_stop.setEnabled(False) if errored: From 0c41983e12d52e30d3a3534930de90814ea10436 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Jun 2020 15:22:46 +0200 Subject: [PATCH 095/131] fix missing variable --- pype/tools/pyblish_pype/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index a6e6cd245e..fc5e8cbef5 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -762,8 +762,8 @@ class Window(QtWidgets.QDialog): ) def on_was_stopped(self): + errored = self.controller.errored if self.controller.collected: - errored = self.controller.errored self.footer_button_play.setEnabled(not errored) self.footer_button_validate.setEnabled( not errored and not self.controller.validated From bd195133d38c613db28c23ab3663f005c9f4a9a8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 17 Jun 2020 15:23:58 +0200 Subject: [PATCH 096/131] switch to default MayaPype plugin, passing more variables, bug fixes --- pype/plugins/global/publish/submit_publish_job.py | 8 ++++++-- pype/plugins/maya/create/create_render.py | 2 +- pype/plugins/maya/publish/collect_render.py | 6 +++--- pype/plugins/maya/publish/submit_maya_deadline.py | 11 +++++++---- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index a05cc3721e..82de2ec099 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -14,7 +14,10 @@ import pyblish.api def _get_script(): """Get path to the image sequence script.""" - from pathlib import Path + try: + from pathlib import Path + except ImportError: + from pathlib2 import Path try: from pype.scripts import publish_filesequence @@ -26,6 +29,7 @@ def _get_script(): module_path = module_path[: -len(".pyc")] + ".py" path = Path(os.path.normpath(module_path)).resolve(strict=True) + assert path is not None, ("Cannot determine path") return str(path) @@ -840,7 +844,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # add audio to metadata file if available audio_file = context.data.get("audioFile") - if os.path.isfile(audio_file): + if audio_file and os.path.isfile(audio_file): publish_job.update({"audio": audio_file}) # pass Ftrack credentials in case of Muster diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index b6eb2e8daa..3b2048d8f0 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -179,7 +179,7 @@ class CreateRender(avalon.maya.Creator): self.data["framesPerTask"] = 1 self.data["whitelist"] = False self.data["machineList"] = "" - self.data["useMayaBatch"] = True + self.data["useMayaBatch"] = False self.data["vrayScene"] = False # Disable for now as this feature is not working yet # self.data["assScene"] = False diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index eed44fbefd..03b14f76bb 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -332,9 +332,9 @@ class CollectMayaRender(pyblish.api.ContextPlugin): options["extendFrames"] = extend_frames options["overrideExistingFrame"] = override_frames - maya_render_plugin = "MayaBatch" - if not attributes.get("useMayaBatch", True): - maya_render_plugin = "MayaCmd" + maya_render_plugin = "MayaPype" + if attributes.get("useMayaBatch", True): + maya_render_plugin = "MayaBatch" options["mayaRenderPlugin"] = maya_render_plugin diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 5a8b2f6e5a..8750d88b90 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -41,7 +41,7 @@ payload_skeleton = { "BatchName": None, # Top-level group name "Name": None, # Job name, as seen in Monitor "UserName": None, - "Plugin": "MayaBatch", + "Plugin": "MayaPype", "Frames": "{start}-{end}x{step}", "Comment": None, }, @@ -274,7 +274,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): step=int(self._instance.data["byFrameStep"])) payload_skeleton["JobInfo"]["Plugin"] = self._instance.data.get( - "mayaRenderPlugin", "MayaBatch") + "mayaRenderPlugin", "MayaPype") payload_skeleton["JobInfo"]["BatchName"] = filename # Job name, as seen in Monitor @@ -311,12 +311,14 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): "AVALON_TASK", "PYPE_USERNAME", "PYPE_DEV", - "PYPE_LOG_NO_COLORS" + "PYPE_LOG_NO_COLORS", + "PYPE_SETUP_PATH" ] environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **api.Session) environment["PYPE_LOG_NO_COLORS"] = "1" + environment["PYPE_MAYA_VERSION"] = cmds.about(v=True) payload_skeleton["JobInfo"].update({ "EnvironmentKeyValue%d" % index: "{key}={value}".format( key=key, @@ -428,7 +430,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): int(self._instance.data["frameStartHandle"]), int(self._instance.data["frameEndHandle"])), - "Plugin": "MayaBatch", + "Plugin": self._instance.data.get( + "mayaRenderPlugin", "MayaPype"), "FramesPerTask": self._instance.data.get("framesPerTask", 1) } From 48085fa8b9cad418c98fe006ded9971a50a2c44b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 17 Jun 2020 15:35:42 +0100 Subject: [PATCH 097/131] Harmony scene validation needs to start at 1. --- pype/plugins/harmony/publish/validate_scene_settings.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/plugins/harmony/publish/validate_scene_settings.py b/pype/plugins/harmony/publish/validate_scene_settings.py index 260d64c42b..aa9a70bd85 100644 --- a/pype/plugins/harmony/publish/validate_scene_settings.py +++ b/pype/plugins/harmony/publish/validate_scene_settings.py @@ -31,6 +31,12 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): def process(self, instance): expected_settings = pype.hosts.harmony.get_asset_settings() + # Harmony is expected to start at 1. + frame_start = expected_settings["frameStart"] + frame_end = expected_settings["frameEnd"] + expected_settings["frameEnd"] = frame_end - frame_start + 1 + expected_settings["frameStart"] = 1 + func = """function func() { return { From 0ab4842c03b0d47e4dfd98d060dd646e2a74a563 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 17 Jun 2020 15:37:57 +0100 Subject: [PATCH 098/131] Load audio - Audio on renders (reviews) --- pype/plugins/harmony/load/load_audio.py | 42 +++++++++++++++++++ .../plugins/harmony/publish/extract_render.py | 5 ++- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 pype/plugins/harmony/load/load_audio.py diff --git a/pype/plugins/harmony/load/load_audio.py b/pype/plugins/harmony/load/load_audio.py new file mode 100644 index 0000000000..a17af78964 --- /dev/null +++ b/pype/plugins/harmony/load/load_audio.py @@ -0,0 +1,42 @@ +from avalon import api, harmony + + +func = """ +function getUniqueColumnName( column_prefix ) +{ + var suffix = 0; + // finds if unique name for a column + var column_name = column_prefix; + while(suffix < 2000) + { + if(!column.type(column_name)) + break; + + suffix = suffix + 1; + column_name = column_prefix + "_" + suffix; + } + return column_name; +} + +function func(args) +{ + var uniqueColumnName = getUniqueColumnName(args[0]); + column.add(uniqueColumnName , "SOUND"); + column.importSound(uniqueColumnName, 1, args[1]); +} +func +""" + + +class ImportAudioLoader(api.Loader): + """Import audio.""" + + families = ["shot"] + representations = ["wav"] + label = "Import Audio" + + def load(self, context, name=None, namespace=None, data=None): + wav_file = api.get_representation_path(context["representation"]) + harmony.send( + {"function": func, "args": [context["subset"]["name"], wav_file]} + ) diff --git a/pype/plugins/harmony/publish/extract_render.py b/pype/plugins/harmony/publish/extract_render.py index 75d0d2ae36..7ca83d3f0f 100644 --- a/pype/plugins/harmony/publish/extract_render.py +++ b/pype/plugins/harmony/publish/extract_render.py @@ -28,7 +28,8 @@ class ExtractRender(pyblish.api.InstancePlugin): scene.currentScene(), scene.getFrameRate(), scene.getStartFrame(), - scene.getStopFrame() + scene.getStopFrame(), + sound.getSoundtrackAll().path() ] } func @@ -41,6 +42,7 @@ class ExtractRender(pyblish.api.InstancePlugin): frame_rate = result[3] frame_start = result[4] frame_end = result[5] + audio_path = result[6] # Set output path to temp folder. path = tempfile.mkdtemp() @@ -111,6 +113,7 @@ class ExtractRender(pyblish.api.InstancePlugin): mov_path = os.path.join(path, instance.data["name"] + ".mov") args = [ "ffmpeg", "-y", + "-i", audio_path, "-i", os.path.join(path, collection.head + "%04d" + collection.tail), mov_path From 7769560504946937d4b060a720915799ddb278a7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Jun 2020 19:20:12 +0200 Subject: [PATCH 099/131] pyblish gui has ability to skip logs with button --- pype/tools/pyblish_pype/model.py | 43 ++++++++++++++++--------------- pype/tools/pyblish_pype/window.py | 11 ++++++-- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index e316d5781a..b9257bfeea 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -1107,35 +1107,36 @@ class TerminalModel(QtGui.QStandardItemModel): def reset(self): self.clear() - def prepare_records(self, result): + def prepare_records(self, result, suspend_logs): prepared_records = [] instance_name = None instance = result["instance"] if instance is not None: instance_name = instance.data["name"] - for record in result.get("records") or []: - if isinstance(record, dict): - record_item = record - else: - record_item = { - "label": text_type(record.msg), - "type": "record", - "levelno": record.levelno, - "threadName": record.threadName, - "name": record.name, - "filename": record.filename, - "pathname": record.pathname, - "lineno": record.lineno, - "msg": text_type(record.msg), - "msecs": record.msecs, - "levelname": record.levelname - } + if not suspend_logs: + for record in result.get("records") or []: + if isinstance(record, dict): + record_item = record + else: + record_item = { + "label": text_type(record.msg), + "type": "record", + "levelno": record.levelno, + "threadName": record.threadName, + "name": record.name, + "filename": record.filename, + "pathname": record.pathname, + "lineno": record.lineno, + "msg": text_type(record.msg), + "msecs": record.msecs, + "levelname": record.levelname + } - if instance_name is not None: - record_item["instance"] = instance_name + if instance_name is not None: + record_item["instance"] = instance_name - prepared_records.append(record_item) + prepared_records.append(record_item) error = result.get("error") if error: diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 701eb41d0d..75b3f31c66 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -54,6 +54,7 @@ class Window(QtWidgets.QDialog): def __init__(self, controller, parent=None): super(Window, self).__init__(parent=parent) + self._suspend_logs = False # Use plastique style for specific ocations # TODO set style name via environment variable low_keys = { @@ -834,7 +835,10 @@ class Window(QtWidgets.QDialog): if self.tabs["artist"].isChecked(): self.tabs["overview"].toggle() - result["records"] = self.terminal_model.prepare_records(result) + result["records"] = self.terminal_model.prepare_records( + result, + self._suspend_logs + ) plugin_item = self.plugin_model.update_with_result(result) instance_item = self.instance_model.update_with_result(result) @@ -933,7 +937,10 @@ class Window(QtWidgets.QDialog): plugin_item = self.plugin_model.plugin_items[result["plugin"].id] action_state = plugin_item.data(Roles.PluginActionProgressRole) action_state |= PluginActionStates.HasFinished - result["records"] = self.terminal_model.prepare_records(result) + result["records"] = self.terminal_model.prepare_records( + result, + self._suspend_logs + ) error = result.get("error") if error: From a3a450330f8516b4bca2a9e28f5054ae883b0e17 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Jun 2020 19:20:40 +0200 Subject: [PATCH 100/131] added button to skip logs --- pype/tools/pyblish_pype/window.py | 60 +++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 75b3f31c66..9aa77a57a8 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -96,6 +96,18 @@ class Window(QtWidgets.QDialog): header_tab_terminal = QtWidgets.QRadioButton(header_tab_widget) header_spacer = QtWidgets.QWidget(header_tab_widget) + button_suspend_logs_widget = QtWidgets.QWidget() + button_suspend_logs_widget_layout = QtWidgets.QHBoxLayout( + button_suspend_logs_widget + ) + button_suspend_logs_widget_layout.setContentsMargins(0, 10, 0, 10) + button_suspend_logs = QtWidgets.QPushButton(header_widget) + button_suspend_logs.setFixedWidth(7) + button_suspend_logs.setSizePolicy( + QtWidgets.QSizePolicy.Preferred, + QtWidgets.QSizePolicy.Expanding + ) + button_suspend_logs_widget_layout.addWidget(button_suspend_logs) header_aditional_btns = QtWidgets.QWidget(header_tab_widget) aditional_btns_layout = QtWidgets.QHBoxLayout(header_aditional_btns) @@ -110,9 +122,11 @@ class Window(QtWidgets.QDialog): layout_tab.addWidget(header_tab_artist, 0) layout_tab.addWidget(header_tab_overview, 0) layout_tab.addWidget(header_tab_terminal, 0) + layout_tab.addWidget(button_suspend_logs_widget, 0) + # Compress items to the left layout_tab.addWidget(header_spacer, 1) - layout_tab.addWidget(header_aditional_btns, 1) + layout_tab.addWidget(header_aditional_btns, 0) layout = QtWidgets.QHBoxLayout(header_widget) layout.setContentsMargins(0, 0, 0, 0) @@ -227,6 +241,10 @@ class Window(QtWidgets.QDialog): footer_info = QtWidgets.QLabel(footer_widget) footer_spacer = QtWidgets.QWidget(footer_widget) + + footer_button_stop = QtWidgets.QPushButton( + awesome["stop"], footer_widget + ) footer_button_reset = QtWidgets.QPushButton( awesome["refresh"], footer_widget ) @@ -236,14 +254,12 @@ class Window(QtWidgets.QDialog): footer_button_play = QtWidgets.QPushButton( awesome["play"], footer_widget ) - footer_button_stop = QtWidgets.QPushButton( - awesome["stop"], footer_widget - ) layout = QtWidgets.QHBoxLayout() layout.setContentsMargins(5, 5, 5, 5) layout.addWidget(footer_info, 0) layout.addWidget(footer_spacer, 1) + layout.addWidget(footer_button_stop, 0) layout.addWidget(footer_button_reset, 0) layout.addWidget(footer_button_validate, 0) @@ -343,10 +359,11 @@ class Window(QtWidgets.QDialog): "TerminalView": terminal_view, # Buttons - "Play": footer_button_play, - "Validate": footer_button_validate, - "Reset": footer_button_reset, + "SuspendLogsBtn": button_suspend_logs, "Stop": footer_button_stop, + "Reset": footer_button_reset, + "Validate": footer_button_validate, + "Play": footer_button_play, # Misc "HeaderSpacer": header_spacer, @@ -371,10 +388,11 @@ class Window(QtWidgets.QDialog): overview_page, terminal_page, footer_widget, - footer_button_play, - footer_button_validate, + button_suspend_logs, footer_button_stop, footer_button_reset, + footer_button_validate, + footer_button_play, footer_spacer, closing_placeholder ): @@ -420,6 +438,7 @@ class Window(QtWidgets.QDialog): overview_instance_view.toggled.connect(self.on_instance_toggle) overview_plugin_view.toggled.connect(self.on_plugin_toggle) + button_suspend_logs.clicked.connect(self.on_suspend_clicked) footer_button_stop.clicked.connect(self.on_stop_clicked) footer_button_reset.clicked.connect(self.on_reset_clicked) footer_button_validate.clicked.connect(self.on_validate_clicked) @@ -443,10 +462,11 @@ class Window(QtWidgets.QDialog): self.terminal_filters_widget = terminal_filters_widget self.footer_widget = footer_widget + self.button_suspend_logs = button_suspend_logs + self.footer_button_stop = footer_button_stop self.footer_button_reset = footer_button_reset self.footer_button_validate = footer_button_validate self.footer_button_play = footer_button_play - self.footer_button_stop = footer_button_stop self.overview_instance_view = overview_instance_view self.overview_plugin_view = overview_plugin_view @@ -613,6 +633,13 @@ class Window(QtWidgets.QDialog): self.footer_button_play.setEnabled(False) self.footer_button_stop.setEnabled(False) + def on_suspend_clicked(self): + self._suspend_logs = not self._suspend_logs + if self.state["current_page"] == "terminal": + self.on_tab_changed("overview") + + self.tabs["terminal"].setVisible(not self._suspend_logs) + def on_comment_entered(self): """The user has typed a comment.""" self.controller.context.data["comment"] = self.comment_box.text() @@ -727,6 +754,8 @@ class Window(QtWidgets.QDialog): self.on_tab_changed(self.state["current_page"]) self.update_compatibility() + self.button_suspend_logs.setEnabled(False) + self.footer_button_validate.setEnabled(True) self.footer_button_reset.setEnabled(True) self.footer_button_stop.setEnabled(False) @@ -776,6 +805,12 @@ class Window(QtWidgets.QDialog): self.footer_widget.setProperty("success", 0) self.footer_widget.style().polish(self.footer_widget) + suspend_log_bool = ( + self.controller.collect_state == 1 + and not self.controller.stopped + ) + self.button_suspend_logs.setEnabled(suspend_log_bool) + def on_was_skipped(self, plugin): plugin_item = self.plugin_model.plugin_items[plugin.id] plugin_item.setData( @@ -896,16 +931,19 @@ class Window(QtWidgets.QDialog): self.footer_button_validate.setEnabled(False) self.footer_button_play.setEnabled(False) + self.button_suspend_logs.setEnabled(False) + util.defer(5, self.controller.validate) def publish(self): self.info(self.tr("Preparing publish..")) - self.footer_button_stop.setEnabled(True) self.footer_button_reset.setEnabled(False) self.footer_button_validate.setEnabled(False) self.footer_button_play.setEnabled(False) + self.button_suspend_logs.setEnabled(False) + util.defer(5, self.controller.publish) def act(self, plugin_item, action): From 2b29562c9c8a84e36d61c8f397b4b4b06c845e97 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Jun 2020 19:20:49 +0200 Subject: [PATCH 101/131] button for skipping logs has style --- pype/tools/pyblish_pype/app.css | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pype/tools/pyblish_pype/app.css b/pype/tools/pyblish_pype/app.css index b52d9efec8..3a2c05c1f3 100644 --- a/pype/tools/pyblish_pype/app.css +++ b/pype/tools/pyblish_pype/app.css @@ -491,3 +491,24 @@ QToolButton { #TerminalFilerBtn[type="log_critical"]:checked {color: rgb(255, 79, 117);} #TerminalFilerBtn[type="log_critical"] {color: rgba(255, 79, 117, 63);} + +#SuspendLogsBtn { + background: #444; + border: none; + border-top-right-radius: 7px; + border-bottom-right-radius: 7px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + font-family: "FontAwesome"; + font-size: 11pt; + color: white; + padding: 0px; +} + +#SuspendLogsBtn:hover { + background: #333; +} + +#SuspendLogsBtn:disabled { + background: #4c4c4c; +} From 86fe1bd06a424e403c13986d0c1840bed4acc9f2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Jun 2020 19:21:04 +0200 Subject: [PATCH 102/131] removed unnecessary attribute --- pype/tools/pyblish_pype/control.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/tools/pyblish_pype/control.py b/pype/tools/pyblish_pype/control.py index a078f0146d..5138b5cc4c 100644 --- a/pype/tools/pyblish_pype/control.py +++ b/pype/tools/pyblish_pype/control.py @@ -86,7 +86,6 @@ class Controller(QtCore.QObject): # - passing collectors order disables plugin/instance toggle self.collectors_order = None self.collect_state = 0 - self.collected = False # - passing validators order disables validate button and gives ability # to know when to stop on validate button press From bcdc7c9634f8b8c907de3b4a11b5c07689096ede Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 18 Jun 2020 15:18:00 +0200 Subject: [PATCH 103/131] reapply suspend logs btn --- pype/tools/pyblish_pype/window.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index c9677f0ffd..c88a7e4fd6 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -754,6 +754,8 @@ class Window(QtWidgets.QDialog): self.on_tab_changed(self.state["current_page"]) self.update_compatibility() + self.button_suspend_logs.setEnabled(False) + self.footer_button_validate.setEnabled(False) self.footer_button_reset.setEnabled(False) self.footer_button_stop.setEnabled(True) From 3063ce29c0ffdf1dd8689876bf09a24a2b394ed8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 18 Jun 2020 15:22:55 +0200 Subject: [PATCH 104/131] changed removed attribute --- pype/tools/pyblish_pype/window.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index c88a7e4fd6..8d4a80107c 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -793,14 +793,14 @@ class Window(QtWidgets.QDialog): def on_was_stopped(self): errored = self.controller.errored - if self.controller.collected: + if self.controller.collect_state == 0: + self.footer_button_play.setEnabled(False) + self.footer_button_validate.setEnabled(False) + else: self.footer_button_play.setEnabled(not errored) self.footer_button_validate.setEnabled( not errored and not self.controller.validated ) - else: - self.footer_button_play.setEnabled(False) - self.footer_button_validate.setEnabled(False) self.footer_button_play.setFocus() self.footer_button_reset.setEnabled(True) From e73db66e6d32af5b3f05d895ac331c999294ca57 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 18 Jun 2020 18:14:18 +0200 Subject: [PATCH 105/131] use `get_ffmpeg_tool_path` for ffmpeg executable to be sure its used pypes ffmpeg --- pype/plugins/standalonepublisher/publish/extract_shot.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pype/plugins/standalonepublisher/publish/extract_shot.py b/pype/plugins/standalonepublisher/publish/extract_shot.py index f2fc2b74cf..d58ddfe8d5 100644 --- a/pype/plugins/standalonepublisher/publish/extract_shot.py +++ b/pype/plugins/standalonepublisher/publish/extract_shot.py @@ -26,8 +26,9 @@ class ExtractShot(pype.api.Extractor): os.path.dirname(editorial_path), basename + ".mov" ) shot_mov = os.path.join(staging_dir, instance.data["name"] + ".mov") + ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") args = [ - "ffmpeg", + ffmpeg_path, "-ss", str(instance.data["frameStart"] / fps), "-i", input_path, "-t", str( @@ -58,7 +59,7 @@ class ExtractShot(pype.api.Extractor): shot_jpegs = os.path.join( staging_dir, instance.data["name"] + ".%04d.jpeg" ) - args = ["ffmpeg", "-i", shot_mov, shot_jpegs] + args = [ffmpeg_path, "-i", shot_mov, shot_jpegs] self.log.info(f"Processing: {args}") output = pype.lib._subprocess(args) self.log.info(output) @@ -79,7 +80,7 @@ class ExtractShot(pype.api.Extractor): # Generate wav file. shot_wav = os.path.join(staging_dir, instance.data["name"] + ".wav") - args = ["ffmpeg", "-i", shot_mov, shot_wav] + args = [ffmpeg_path, "-i", shot_mov, shot_wav] self.log.info(f"Processing: {args}") output = pype.lib._subprocess(args) self.log.info(output) From 511b2d1e24b1d463c18136eecc18f228aa03c7fd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 18 Jun 2020 22:33:29 +0100 Subject: [PATCH 106/131] Cloud Mongo There was a circular import when importing from pypeapp, which I couldnt resolve, so have copied the mongo url logic across. --- pype/api.py | 8 +- pype/lib.py | 81 +++++++++++++++++++ .../adobe_communicator/lib/io_nonsingleton.py | 10 ++- pype/modules/avalon_apps/rest_api.py | 5 +- .../ftrack/ftrack_server/event_server_cli.py | 31 ++++--- pype/modules/ftrack/ftrack_server/lib.py | 72 ++--------------- .../ftrack/ftrack_server/sub_event_storer.py | 1 - .../modules/ftrack/lib/custom_db_connector.py | 15 ++-- pype/modules/ftrack/lib/io_nonsingleton.py | 5 +- schema/session-2.0.json | 7 -- 10 files changed, 131 insertions(+), 104 deletions(-) diff --git a/pype/api.py b/pype/api.py index 200ca9daec..f279bb501a 100644 --- a/pype/api.py +++ b/pype/api.py @@ -32,7 +32,10 @@ from .lib import ( get_version_from_path, get_last_version_from_path, modified_environ, - add_tool_to_environment + add_tool_to_environment, + decompose_url, + compose_url, + get_default_components ) # Special naming case for subprocess since its a built-in method. @@ -44,6 +47,9 @@ __all__ = [ "project_overrides_dir_path", "config", "execute", + "decompose_url", + "compose_url", + "get_default_components", # plugin classes "Extractor", diff --git a/pype/lib.py b/pype/lib.py index 7c7a01d5cc..a41a082bc0 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1386,3 +1386,84 @@ def ffprobe_streams(path_to_file): popen_output = popen.communicate()[0] log.debug("FFprobe output: {}".format(popen_output)) return json.loads(popen_output)["streams"] + + +import os + +try: + from urllib.parse import urlparse, parse_qs +except ImportError: + from urlparse import urlparse, parse_qs + + +def decompose_url(url): + components = { + "scheme": None, + "host": None, + "port": None, + "username": None, + "password": None, + "auth_db": "" + } + + result = urlparse(url) + + components["scheme"] = result.scheme + components["host"] = result.hostname + try: + components["port"] = result.port + except ValueError: + raise RuntimeError("invalid port specified") + components["username"] = result.username + components["password"] = result.password + + try: + components["auth_db"] = parse_qs(result.query)['authSource'][0] + except KeyError: + # no auth db provided, mongo will use the one we are connecting to + pass + + return components + + +def compose_url(scheme=None, + host=None, + username=None, + password=None, + database=None, + collection=None, + port=None, + auth_db=""): + + url = "{scheme}://" + + if username and password: + url += "{username}:{password}@" + + url += "{host}" + + if database: + url += "/{database}" + + if database and collection: + url += "/{collection}" + + if port: + url += ":{port}" + + url += auth_db + + return url.format(**{ + "scheme": scheme, + "host": host, + "username": username, + "password": password, + "database": database, + "collection": collection, + "port": port, + "auth_db": "" + }) + + +def get_default_components(): + return decompose_url(os.environ["MONGO_URL"]) diff --git a/pype/modules/adobe_communicator/lib/io_nonsingleton.py b/pype/modules/adobe_communicator/lib/io_nonsingleton.py index 6380e4eb23..d042d2f6d8 100644 --- a/pype/modules/adobe_communicator/lib/io_nonsingleton.py +++ b/pype/modules/adobe_communicator/lib/io_nonsingleton.py @@ -16,6 +16,7 @@ import contextlib from avalon import schema from avalon.vendor import requests +from pype.api import get_default_components, compose_url # Third-party dependencies import pymongo @@ -72,8 +73,15 @@ class DbConnector(object): self.Session.update(self._from_environment()) timeout = int(self.Session["AVALON_TIMEOUT"]) + + components = get_default_components() + port = components.pop("port") + host = compose_url(**components) self._mongo_client = pymongo.MongoClient( - self.Session["AVALON_MONGO"], serverSelectionTimeoutMS=timeout) + host=host, + port=port, + serverSelectionTimeoutMS=timeout + ) for retry in range(3): try: diff --git a/pype/modules/avalon_apps/rest_api.py b/pype/modules/avalon_apps/rest_api.py index a5dc326a8e..58cb3a47f3 100644 --- a/pype/modules/avalon_apps/rest_api.py +++ b/pype/modules/avalon_apps/rest_api.py @@ -8,10 +8,7 @@ from pype.modules.ftrack.lib.custom_db_connector import DbConnector class AvalonRestApi(RestApi): - dbcon = DbConnector( - os.environ["AVALON_MONGO"], - os.environ["AVALON_DB"] - ) + dbcon = DbConnector(os.environ["AVALON_DB"]) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/pype/modules/ftrack/ftrack_server/event_server_cli.py b/pype/modules/ftrack/ftrack_server/event_server_cli.py index 5709a88297..7ca04756df 100644 --- a/pype/modules/ftrack/ftrack_server/event_server_cli.py +++ b/pype/modules/ftrack/ftrack_server/event_server_cli.py @@ -13,10 +13,10 @@ import time import uuid import ftrack_api +import pymongo from pype.modules.ftrack.lib import credentials -from pype.modules.ftrack.ftrack_server.lib import ( - ftrack_events_mongo_settings, check_ftrack_url -) +from pype.modules.ftrack.ftrack_server.lib import check_ftrack_url +from pype.api import get_default_components, compose_url import socket_thread @@ -30,22 +30,19 @@ class MongoPermissionsError(Exception): def check_mongo_url(host, port, log_error=False): """Checks if mongo server is responding""" - sock = None try: - sock = socket.create_connection( - (host, port), - timeout=1 - ) - return True - except socket.error as err: + client = pymongo.MongoClient(host=host, port=port) + # Force connection on a request as the connect=True parameter of + # MongoClient seems to be useless here + client.server_info() + except pymongo.errors.ServerSelectionTimeoutError as err: if log_error: print("Can't connect to MongoDB at {}:{} because: {}".format( host, port, err )) return False - finally: - if sock is not None: - sock.close() + + return True def validate_credentials(url, user, api): @@ -190,9 +187,9 @@ def main_loop(ftrack_url): os.environ["FTRACK_EVENT_SUB_ID"] = str(uuid.uuid1()) # Get mongo hostname and port for testing mongo connection - mongo_list = ftrack_events_mongo_settings() - mongo_hostname = mongo_list[0] - mongo_port = mongo_list[1] + components = get_default_components() + mongo_port = components.pop("port") + mongo_hostname = compose_url(**components) # Current file file_path = os.path.dirname(os.path.realpath(__file__)) @@ -275,7 +272,7 @@ def main_loop(ftrack_url): # Run threads only if Ftrack is accessible if not ftrack_accessible or not mongo_accessible: if not mongo_accessible and not printed_mongo_error: - mongo_url = mongo_hostname + ":" + mongo_port + mongo_url = "{}:{}".format(mongo_hostname, mongo_port) print("Can't access Mongo {}".format(mongo_url)) if not ftrack_accessible and not printed_ftrack_error: diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py index 129cd7173a..b7383dab07 100644 --- a/pype/modules/ftrack/ftrack_server/lib.py +++ b/pype/modules/ftrack/ftrack_server/lib.py @@ -18,12 +18,8 @@ import ftrack_api.operation import ftrack_api._centralized_storage_scenario import ftrack_api.event from ftrack_api.logging import LazyLogMessage as L -try: - from urllib.parse import urlparse, parse_qs -except ImportError: - from urlparse import urlparse, parse_qs -from pype.api import Logger +from pype.api import Logger, compose_url, get_default_components from pype.modules.ftrack.lib.custom_db_connector import DbConnector @@ -32,67 +28,10 @@ TOPIC_STATUS_SERVER = "pype.event.server.status" TOPIC_STATUS_SERVER_RESULT = "pype.event.server.status.result" -def ftrack_events_mongo_settings(): - host = None - port = None - username = None - password = None - collection = None - database = None - auth_db = "" - - if os.environ.get('FTRACK_EVENTS_MONGO_URL'): - result = urlparse(os.environ['FTRACK_EVENTS_MONGO_URL']) - - host = result.hostname - try: - port = result.port - except ValueError: - raise RuntimeError("invalid port specified") - username = result.username - password = result.password - try: - database = result.path.lstrip("/").split("/")[0] - collection = result.path.lstrip("/").split("/")[1] - except IndexError: - if not database: - raise RuntimeError("missing database name for logging") - try: - auth_db = parse_qs(result.query)['authSource'][0] - except KeyError: - # no auth db provided, mongo will use the one we are connecting to - pass - else: - host = os.environ.get('FTRACK_EVENTS_MONGO_HOST') - port = int(os.environ.get('FTRACK_EVENTS_MONGO_PORT', "0")) - database = os.environ.get('FTRACK_EVENTS_MONGO_DB') - username = os.environ.get('FTRACK_EVENTS_MONGO_USER') - password = os.environ.get('FTRACK_EVENTS_MONGO_PASSWORD') - collection = os.environ.get('FTRACK_EVENTS_MONGO_COL') - auth_db = os.environ.get('FTRACK_EVENTS_MONGO_AUTH_DB', 'avalon') - - return host, port, database, username, password, collection, auth_db - - def get_ftrack_event_mongo_info(): - host, port, database, username, password, collection, auth_db = ( - ftrack_events_mongo_settings() - ) - user_pass = "" - if username and password: - user_pass = "{}:{}@".format(username, password) - - socket_path = "{}:{}".format(host, port) - - dab = "" - if database: - dab = "/{}".format(database) - - auth = "" - if auth_db: - auth = "?authSource={}".format(auth_db) - - url = "mongodb://{}{}{}{}".format(user_pass, socket_path, dab, auth) + url = compose_url(get_default_components()) + database = os.environ["FTRACK_EVENTS_MONGO_DB"] + collection = os.environ["FTRACK_EVENTS_MONGO_COL"] return url, database, collection @@ -205,7 +144,6 @@ class ProcessEventHub(SocketBaseEventHub): def __init__(self, *args, **kwargs): self.dbcon = DbConnector( - mongo_url=self.url, database_name=self.database, table_name=self.table_name ) @@ -269,7 +207,7 @@ class ProcessEventHub(SocketBaseEventHub): def load_events(self): """Load not processed events sorted by stored date""" ago_date = datetime.datetime.now() - datetime.timedelta(days=3) - result = self.dbcon.delete_many({ + self.dbcon.delete_many({ "pype_data.stored": {"$lte": ago_date}, "pype_data.is_processed": True }) diff --git a/pype/modules/ftrack/ftrack_server/sub_event_storer.py b/pype/modules/ftrack/ftrack_server/sub_event_storer.py index c4d199407d..727d5aa515 100644 --- a/pype/modules/ftrack/ftrack_server/sub_event_storer.py +++ b/pype/modules/ftrack/ftrack_server/sub_event_storer.py @@ -25,7 +25,6 @@ class SessionFactory: url, database, table_name = get_ftrack_event_mongo_info() dbcon = DbConnector( - mongo_url=url, database_name=database, table_name=table_name ) diff --git a/pype/modules/ftrack/lib/custom_db_connector.py b/pype/modules/ftrack/lib/custom_db_connector.py index b307117127..e570e4da38 100644 --- a/pype/modules/ftrack/lib/custom_db_connector.py +++ b/pype/modules/ftrack/lib/custom_db_connector.py @@ -13,6 +13,8 @@ import atexit # Third-party dependencies import pymongo +from pype.api import get_default_components, compose_url + class NotActiveTable(Exception): def __init__(self, *args, **kwargs): @@ -63,13 +65,12 @@ class DbConnector: log = logging.getLogger(__name__) timeout = 1000 - def __init__(self, mongo_url, database_name, table_name=None): + def __init__(self, database_name, table_name=None): self._mongo_client = None self._sentry_client = None self._sentry_logging_handler = None self._database = None self._is_installed = False - self._mongo_url = mongo_url self._database_name = database_name self.active_table = table_name @@ -95,8 +96,12 @@ class DbConnector: atexit.register(self.uninstall) logging.basicConfig() + components = get_default_components() + port = components.pop("port") + host = compose_url(**components) self._mongo_client = pymongo.MongoClient( - self._mongo_url, + host=host, + port=port, serverSelectionTimeoutMS=self.timeout ) @@ -113,11 +118,11 @@ class DbConnector: else: raise IOError( "ERROR: Couldn't connect to %s in " - "less than %.3f ms" % (self._mongo_url, self.timeout) + "less than %.3f ms" % (host, self.timeout) ) self.log.info("Connected to %s, delay %.3f s" % ( - self._mongo_url, time.time() - t1 + host, time.time() - t1 )) self._database = self._mongo_client[self._database_name] diff --git a/pype/modules/ftrack/lib/io_nonsingleton.py b/pype/modules/ftrack/lib/io_nonsingleton.py index 6380e4eb23..73856557ea 100644 --- a/pype/modules/ftrack/lib/io_nonsingleton.py +++ b/pype/modules/ftrack/lib/io_nonsingleton.py @@ -73,7 +73,10 @@ class DbConnector(object): timeout = int(self.Session["AVALON_TIMEOUT"]) self._mongo_client = pymongo.MongoClient( - self.Session["AVALON_MONGO"], serverSelectionTimeoutMS=timeout) + host=os.environ["AVALON_MONGO_HOST"], + port=int(os.environ["AVALON_MONGO_PORT"]), + serverSelectionTimeoutMS=timeout + ) for retry in range(3): try: diff --git a/schema/session-2.0.json b/schema/session-2.0.json index d37f2ac822..7ad2c63bcf 100644 --- a/schema/session-2.0.json +++ b/schema/session-2.0.json @@ -56,13 +56,6 @@ "pattern": "^\\w*$", "example": "maya2016" }, - "AVALON_MONGO": { - "description": "Address to the asset database", - "type": "string", - "pattern": "^mongodb://[\\w/@:.]*$", - "example": "mongodb://localhost:27017", - "default": "mongodb://localhost:27017" - }, "AVALON_DB": { "description": "Name of database", "type": "string", From c3bf82ac1c9ed63acda481a557d5e3c03a04f77a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 18 Jun 2020 22:37:45 +0100 Subject: [PATCH 107/131] Hound --- pype/lib.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index a41a082bc0..1323897d37 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -17,6 +17,11 @@ import six import avalon.api from .api import config +try: + from urllib.parse import urlparse, parse_qs +except ImportError: + from urlparse import urlparse, parse_qs + log = logging.getLogger(__name__) @@ -1388,14 +1393,6 @@ def ffprobe_streams(path_to_file): return json.loads(popen_output)["streams"] -import os - -try: - from urllib.parse import urlparse, parse_qs -except ImportError: - from urlparse import urlparse, parse_qs - - def decompose_url(url): components = { "scheme": None, From e04345480549cdfc302117e34165d8324f1907d6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 18 Jun 2020 22:57:43 +0100 Subject: [PATCH 108/131] Make ftrack database and collection optional. --- pype/modules/ftrack/ftrack_server/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py index b7383dab07..742976104e 100644 --- a/pype/modules/ftrack/ftrack_server/lib.py +++ b/pype/modules/ftrack/ftrack_server/lib.py @@ -30,8 +30,8 @@ TOPIC_STATUS_SERVER_RESULT = "pype.event.server.status.result" def get_ftrack_event_mongo_info(): url = compose_url(get_default_components()) - database = os.environ["FTRACK_EVENTS_MONGO_DB"] - collection = os.environ["FTRACK_EVENTS_MONGO_COL"] + database = os.environ.get("FTRACK_EVENTS_MONGO_DB") or "pype" + collection = os.environ.get("FTRACK_EVENTS_MONGO_COL") or "ftrack_events" return url, database, collection From 335b1fecaac6fd5f83a33b6f18fefeaf3aca01d9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 19 Jun 2020 08:57:48 +0100 Subject: [PATCH 109/131] Improve query --- pype/lib.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index 1323897d37..ef70aa59cd 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1400,7 +1400,7 @@ def decompose_url(url): "port": None, "username": None, "password": None, - "auth_db": "" + "query": None } result = urlparse(url) @@ -1411,14 +1411,10 @@ def decompose_url(url): components["port"] = result.port except ValueError: raise RuntimeError("invalid port specified") + components["username"] = result.username components["password"] = result.password - - try: - components["auth_db"] = parse_qs(result.query)['authSource'][0] - except KeyError: - # no auth db provided, mongo will use the one we are connecting to - pass + components["query"] = result.query return components @@ -1430,7 +1426,7 @@ def compose_url(scheme=None, database=None, collection=None, port=None, - auth_db=""): + query=None): url = "{scheme}://" @@ -1448,7 +1444,8 @@ def compose_url(scheme=None, if port: url += ":{port}" - url += auth_db + if query: + url += "?{}".format(query) return url.format(**{ "scheme": scheme, @@ -1458,7 +1455,7 @@ def compose_url(scheme=None, "database": database, "collection": collection, "port": port, - "auth_db": "" + "query": query }) From 3c3a9bb780c57111c8f35c5a2fb31a1e58b76711 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 19 Jun 2020 08:58:39 +0100 Subject: [PATCH 110/131] Hound --- pype/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index ef70aa59cd..540a28afc3 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -18,9 +18,9 @@ import avalon.api from .api import config try: - from urllib.parse import urlparse, parse_qs + from urllib.parse import urlparse except ImportError: - from urlparse import urlparse, parse_qs + from urlparse import urlparse log = logging.getLogger(__name__) From f3a435ee400d44c9e79f74431185f985d6486559 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 19 Jun 2020 09:28:04 +0100 Subject: [PATCH 111/131] Increment workfile and sneaky code cosmetic for Harmony. --- .../harmony/publish/increment_workfile.py | 3 +- .../photoshop/publish/increment_workfile.py | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 pype/plugins/photoshop/publish/increment_workfile.py diff --git a/pype/plugins/harmony/publish/increment_workfile.py b/pype/plugins/harmony/publish/increment_workfile.py index 29bae09df3..858e5fab0e 100644 --- a/pype/plugins/harmony/publish/increment_workfile.py +++ b/pype/plugins/harmony/publish/increment_workfile.py @@ -22,8 +22,7 @@ class IncrementWorkfile(pyblish.api.InstancePlugin): errored_plugins = get_errored_plugins_from_data(instance.context) if errored_plugins: raise RuntimeError( - "Skipping incrementing current file because submission to" - " deadline failed." + "Skipping incrementing current file because publishing failed." ) scene_dir = version_up( diff --git a/pype/plugins/photoshop/publish/increment_workfile.py b/pype/plugins/photoshop/publish/increment_workfile.py new file mode 100644 index 0000000000..ba9ab8606a --- /dev/null +++ b/pype/plugins/photoshop/publish/increment_workfile.py @@ -0,0 +1,29 @@ +import pyblish.api +from pype.action import get_errored_plugins_from_data +from pype.lib import version_up +from avalon import photoshop + + +class IncrementWorkfile(pyblish.api.InstancePlugin): + """Increment the current workfile. + + Saves the current scene with an increased version number. + """ + + label = "Increment Workfile" + order = pyblish.api.IntegratorOrder + 9.0 + hosts = ["photoshop"] + families = ["workfile"] + optional = True + + def process(self, instance): + errored_plugins = get_errored_plugins_from_data(instance.context) + if errored_plugins: + raise RuntimeError( + "Skipping incrementing current file because publishing failed." + ) + + scene_path = version_up(instance.context.data["currentFile"]) + photoshop.app().ActiveDocument.SaveAs(scene_path) + + self.log.info("Incremented workfile to: {}".format(scene_path)) From ac78278a950d1ed0d0569e60f1fbb24f346e4e11 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 19 Jun 2020 12:18:03 +0100 Subject: [PATCH 112/131] Defaults for frameStart and frameEnd Only warn users about missing attributes. --- .../global/publish/collect_avalon_entities.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pype/plugins/global/publish/collect_avalon_entities.py b/pype/plugins/global/publish/collect_avalon_entities.py index 51dd3d7b06..917172d40c 100644 --- a/pype/plugins/global/publish/collect_avalon_entities.py +++ b/pype/plugins/global/publish/collect_avalon_entities.py @@ -48,8 +48,18 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin): data = asset_entity['data'] - context.data["frameStart"] = data.get("frameStart") - context.data["frameEnd"] = data.get("frameEnd") + frame_start = data.get("frameStart") + if frame_start is None: + frame_start = 1 + self.log.warning("Missing frame start. Defaulting to 1.") + + frame_end = data.get("frameEnd") + if frame_end is None: + frame_end = 2 + self.log.warning("Missing frame end. Defaulting to 2.") + + context.data["frameStart"] = frame_start + context.data["frameEnd"] = frame_end handles = data.get("handles") or 0 handle_start = data.get("handleStart") @@ -72,7 +82,7 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin): context.data["handleStart"] = int(handle_start) context.data["handleEnd"] = int(handle_end) - frame_start_h = data.get("frameStart") - context.data["handleStart"] - frame_end_h = data.get("frameEnd") + context.data["handleEnd"] + frame_start_h = frame_start - context.data["handleStart"] + frame_end_h = frame_end + context.data["handleEnd"] context.data["frameStartHandle"] = frame_start_h context.data["frameEndHandle"] = frame_end_h From eaf003cbd60386c538c28deb982bd018280a510c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 19 Jun 2020 14:27:41 +0200 Subject: [PATCH 113/131] widgets are imported in view.py when are used --- pype/tools/pyblish_pype/view.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pype/tools/pyblish_pype/view.py b/pype/tools/pyblish_pype/view.py index 03509604bb..450f56421c 100644 --- a/pype/tools/pyblish_pype/view.py +++ b/pype/tools/pyblish_pype/view.py @@ -1,7 +1,14 @@ from Qt import QtCore, QtWidgets from . import model -from . import widgets from .constants import Roles +# Imported when used +widgets = None + + +def _import_widgets(): + global widgets + if widgets is None: + from . import widgets class ArtistView(QtWidgets.QListView): @@ -152,6 +159,8 @@ class TerminalView(QtWidgets.QTreeView): self.clicked.connect(self.item_expand) + _import_widgets() + def event(self, event): if not event.type() == QtCore.QEvent.KeyPress: return super(TerminalView, self).event(event) From 36aee8395bbfc893c8e8fd7c18a0de418b7d8497 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 19 Jun 2020 15:32:02 +0200 Subject: [PATCH 114/131] warnings visualizaion is working again --- pype/tools/pyblish_pype/model.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index b9257bfeea..203b512d12 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -490,12 +490,8 @@ class PluginModel(QtGui.QStandardItemModel): new_records = result.get("records") or [] if not has_warning: for record in new_records: - if not hasattr(record, "levelname"): - continue - - if str(record.levelname).lower() in [ - "warning", "critical", "error" - ]: + level_no = record.get("levelno") + if level_no and level_no >= 30: new_flag_states[PluginStates.HasWarning] = True break @@ -789,12 +785,8 @@ class InstanceModel(QtGui.QStandardItemModel): new_records = result.get("records") or [] if not has_warning: for record in new_records: - if not hasattr(record, "levelname"): - continue - - if str(record.levelname).lower() in [ - "warning", "critical", "error" - ]: + level_no = record.get("levelno") + if level_no and level_no >= 30: new_flag_states[InstanceStates.HasWarning] = True break From c83b939bfff8f63deab440c0d733e4924d998ec3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 19 Jun 2020 15:32:38 +0200 Subject: [PATCH 115/131] appending error message to logs is skipped in action handler because is already done in terminal model --- pype/tools/pyblish_pype/window.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 8d4a80107c..44472f8e1d 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -984,22 +984,8 @@ class Window(QtWidgets.QDialog): self._suspend_logs ) - error = result.get("error") - if error: - records = result.get("records") or [] + if "error" in result: action_state |= PluginActionStates.HasFailed - fname, line_no, func, exc = error.traceback - - records.append({ - "label": str(error), - "type": "error", - "filename": str(fname), - "lineno": str(line_no), - "func": str(func), - "traceback": error.formatted_traceback - }) - - result["records"] = records plugin_item.setData(action_state, Roles.PluginActionProgressRole) From e33999ef486e0c988b8033667bdc34f6590710b1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 19 Jun 2020 15:32:57 +0200 Subject: [PATCH 116/131] perspective widget is updated after action processing --- pype/tools/pyblish_pype/window.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 44472f8e1d..2967488572 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -989,9 +989,14 @@ class Window(QtWidgets.QDialog): plugin_item.setData(action_state, Roles.PluginActionProgressRole) - self.plugin_model.update_with_result(result) - self.instance_model.update_with_result(result) self.terminal_model.update_with_result(result) + plugin_item = self.plugin_model.update_with_result(result) + instance_item = self.instance_model.update_with_result(result) + + if self.perspective_widget.isVisible(): + self.perspective_widget.update_context( + plugin_item, instance_item + ) def closeEvent(self, event): """Perform post-flight checks before closing From 9b9063b7d1450e30ec66b8b7323a22d9e392a51a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 19 Jun 2020 17:09:27 +0100 Subject: [PATCH 117/131] Fix logging --- pype/modules/logging/gui/models.py | 8 +++----- pype/modules/logging/tray/logging_module.py | 15 +++++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pype/modules/logging/gui/models.py b/pype/modules/logging/gui/models.py index 484fd6dc69..945b76152b 100644 --- a/pype/modules/logging/gui/models.py +++ b/pype/modules/logging/gui/models.py @@ -1,8 +1,7 @@ -import os import collections from Qt import QtCore from pype.api import Logger -from pypeapp.lib.log import _bootstrap_mongo_log +from pypeapp.lib.log import _bootstrap_mongo_log, COLLECTION log = Logger().get_logger("LogModel", "LoggingModule") @@ -41,11 +40,10 @@ class LogModel(QtCore.QAbstractItemModel): super(LogModel, self).__init__(parent) self._root_node = Node() - collection = os.environ.get('PYPE_LOG_MONGO_COL') database = _bootstrap_mongo_log() self.dbcon = None - if collection in database.list_collection_names(): - self.dbcon = database[collection] + if COLLECTION in database.list_collection_names(): + self.dbcon = database[COLLECTION] def add_log(self, log): node = Node(log) diff --git a/pype/modules/logging/tray/logging_module.py b/pype/modules/logging/tray/logging_module.py index 087a51f322..9b26d5d9bf 100644 --- a/pype/modules/logging/tray/logging_module.py +++ b/pype/modules/logging/tray/logging_module.py @@ -1,20 +1,23 @@ from Qt import QtWidgets - from pype.api import Logger - from ..gui.app import LogsWindow -log = Logger().get_logger("LoggingModule", "logging") - class LoggingModule: def __init__(self, main_parent=None, parent=None): self.parent = parent + self.log = Logger().get_logger(self.__class__.__name__, "logging") - self.window = LogsWindow() + try: + self.window = LogsWindow() + self.tray_menu = self._tray_menu + except Exception: + self.log.warning( + "Couldn't set Logging GUI due to error.", exc_info=True + ) # Definition of Tray menu - def tray_menu(self, parent_menu): + def _tray_menu(self, parent_menu): # Menu for Tray App menu = QtWidgets.QMenu('Logging', parent_menu) # menu.setProperty('submenu', 'on') From d665d822ae150c8e8d3c662d0522ebea52b0e3b1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:45:03 +0200 Subject: [PATCH 118/131] removed decompose_url, compose_url and get_default_components from pype.lib because are duplicated with pypeapp --- pype/lib.py | 74 ----------------------------------------------------- 1 file changed, 74 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index 540a28afc3..87808e53f5 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -17,10 +17,6 @@ import six import avalon.api from .api import config -try: - from urllib.parse import urlparse -except ImportError: - from urlparse import urlparse log = logging.getLogger(__name__) @@ -1391,73 +1387,3 @@ def ffprobe_streams(path_to_file): popen_output = popen.communicate()[0] log.debug("FFprobe output: {}".format(popen_output)) return json.loads(popen_output)["streams"] - - -def decompose_url(url): - components = { - "scheme": None, - "host": None, - "port": None, - "username": None, - "password": None, - "query": None - } - - result = urlparse(url) - - components["scheme"] = result.scheme - components["host"] = result.hostname - try: - components["port"] = result.port - except ValueError: - raise RuntimeError("invalid port specified") - - components["username"] = result.username - components["password"] = result.password - components["query"] = result.query - - return components - - -def compose_url(scheme=None, - host=None, - username=None, - password=None, - database=None, - collection=None, - port=None, - query=None): - - url = "{scheme}://" - - if username and password: - url += "{username}:{password}@" - - url += "{host}" - - if database: - url += "/{database}" - - if database and collection: - url += "/{collection}" - - if port: - url += ":{port}" - - if query: - url += "?{}".format(query) - - return url.format(**{ - "scheme": scheme, - "host": host, - "username": username, - "password": password, - "database": database, - "collection": collection, - "port": port, - "query": query - }) - - -def get_default_components(): - return decompose_url(os.environ["MONGO_URL"]) From 1c31e768f36eda4414361eb450d37134e40c621f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:45:23 +0200 Subject: [PATCH 119/131] decompose_url, compose_url, get_default_components are imported from pypeapp in pype.api --- pype/api.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pype/api.py b/pype/api.py index f279bb501a..4b3d50044f 100644 --- a/pype/api.py +++ b/pype/api.py @@ -6,6 +6,12 @@ from pypeapp import ( execute ) +from pypeapp.lib.mongo import ( + decompose_url, + compose_url, + get_default_components +) + from .plugin import ( Extractor, @@ -32,10 +38,7 @@ from .lib import ( get_version_from_path, get_last_version_from_path, modified_environ, - add_tool_to_environment, - decompose_url, - compose_url, - get_default_components + add_tool_to_environment ) # Special naming case for subprocess since its a built-in method. @@ -49,7 +52,6 @@ __all__ = [ "execute", "decompose_url", "compose_url", - "get_default_components", # plugin classes "Extractor", From b4b3ed4bf44dd1d11178ab1d43ec6e037378ba16 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:49:19 +0200 Subject: [PATCH 120/131] custom db connector can expect different mongo url --- .../modules/ftrack/lib/custom_db_connector.py | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/pype/modules/ftrack/lib/custom_db_connector.py b/pype/modules/ftrack/lib/custom_db_connector.py index e570e4da38..a734b3f80a 100644 --- a/pype/modules/ftrack/lib/custom_db_connector.py +++ b/pype/modules/ftrack/lib/custom_db_connector.py @@ -12,8 +12,7 @@ import atexit # Third-party dependencies import pymongo - -from pype.api import get_default_components, compose_url +from pype.api import decompose_url class NotActiveTable(Exception): @@ -65,12 +64,29 @@ class DbConnector: log = logging.getLogger(__name__) timeout = 1000 - def __init__(self, database_name, table_name=None): + def __init__( + self, uri, port=None, database_name=None, table_name=None + ): self._mongo_client = None self._sentry_client = None self._sentry_logging_handler = None self._database = None self._is_installed = False + + self._uri = uri + components = decompose_url(uri) + if port is None: + port = components.get("port") + + if database_name is None: + database_name = components.get("database") + + if database_name is None: + raise ValueError( + "Database is not defined for connection. {}".format(uri) + ) + + self._port = port self._database_name = database_name self.active_table = table_name @@ -96,14 +112,16 @@ class DbConnector: atexit.register(self.uninstall) logging.basicConfig() - components = get_default_components() - port = components.pop("port") - host = compose_url(**components) - self._mongo_client = pymongo.MongoClient( - host=host, - port=port, - serverSelectionTimeoutMS=self.timeout - ) + kwargs = { + "host": self._uri, + "serverSelectionTimeoutMS": self.timeout + } + if self._port is not None: + kwargs["port"] = self._port + + self._mongo_client = pymongo.MongoClient(**kwargs) + if self._port is None: + self._port = self._mongo_client.PORT for retry in range(3): try: @@ -118,11 +136,11 @@ class DbConnector: else: raise IOError( "ERROR: Couldn't connect to %s in " - "less than %.3f ms" % (host, self.timeout) + "less than %.3f ms" % (self._uri, self.timeout) ) self.log.info("Connected to %s, delay %.3f s" % ( - host, time.time() - t1 + self._uri, time.time() - t1 )) self._database = self._mongo_client[self._database_name] From d185b5d54c4364846c884a15cf5c4e0d04be0542 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:52:04 +0200 Subject: [PATCH 121/131] io_nonsingleton is same as avalon's io --- .../adobe_communicator/lib/io_nonsingleton.py | 24 ++++++++++++------- pype/modules/ftrack/lib/io_nonsingleton.py | 21 ++++++++++++---- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/pype/modules/adobe_communicator/lib/io_nonsingleton.py b/pype/modules/adobe_communicator/lib/io_nonsingleton.py index d042d2f6d8..da37c657c6 100644 --- a/pype/modules/adobe_communicator/lib/io_nonsingleton.py +++ b/pype/modules/adobe_communicator/lib/io_nonsingleton.py @@ -16,7 +16,7 @@ import contextlib from avalon import schema from avalon.vendor import requests -from pype.api import get_default_components, compose_url +from avalon.io import extract_port_from_url # Third-party dependencies import pymongo @@ -73,15 +73,17 @@ class DbConnector(object): self.Session.update(self._from_environment()) timeout = int(self.Session["AVALON_TIMEOUT"]) + mongo_url = self.Session["AVALON_MONGO"] + kwargs = { + "host": mongo_url, + "serverSelectionTimeoutMS": timeout + } - components = get_default_components() - port = components.pop("port") - host = compose_url(**components) - self._mongo_client = pymongo.MongoClient( - host=host, - port=port, - serverSelectionTimeoutMS=timeout - ) + port = extract_port_from_url(mongo_url) + if port is not None: + kwargs["port"] = int(port) + + self._mongo_client = pymongo.MongoClient(**kwargs) for retry in range(3): try: @@ -389,6 +391,10 @@ class DbConnector(object): if document is None: break + if document.get("type") == "master_version": + _document = self.find_one({"_id": document["version_id"]}) + document["data"] = _document["data"] + parents.append(document) return parents diff --git a/pype/modules/ftrack/lib/io_nonsingleton.py b/pype/modules/ftrack/lib/io_nonsingleton.py index 73856557ea..da37c657c6 100644 --- a/pype/modules/ftrack/lib/io_nonsingleton.py +++ b/pype/modules/ftrack/lib/io_nonsingleton.py @@ -16,6 +16,7 @@ import contextlib from avalon import schema from avalon.vendor import requests +from avalon.io import extract_port_from_url # Third-party dependencies import pymongo @@ -72,11 +73,17 @@ class DbConnector(object): self.Session.update(self._from_environment()) timeout = int(self.Session["AVALON_TIMEOUT"]) - self._mongo_client = pymongo.MongoClient( - host=os.environ["AVALON_MONGO_HOST"], - port=int(os.environ["AVALON_MONGO_PORT"]), - serverSelectionTimeoutMS=timeout - ) + mongo_url = self.Session["AVALON_MONGO"] + kwargs = { + "host": mongo_url, + "serverSelectionTimeoutMS": timeout + } + + port = extract_port_from_url(mongo_url) + if port is not None: + kwargs["port"] = int(port) + + self._mongo_client = pymongo.MongoClient(**kwargs) for retry in range(3): try: @@ -384,6 +391,10 @@ class DbConnector(object): if document is None: break + if document.get("type") == "master_version": + _document = self.find_one({"_id": document["version_id"]}) + document["data"] = _document["data"] + parents.append(document) return parents From 3148115f0a079688f47907a5dd2df70aba8ab091 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:53:10 +0200 Subject: [PATCH 122/131] avalon rest api use io_nonsingleton instead of custom_db_connector --- pype/modules/avalon_apps/rest_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/modules/avalon_apps/rest_api.py b/pype/modules/avalon_apps/rest_api.py index 58cb3a47f3..1cb9e544a7 100644 --- a/pype/modules/avalon_apps/rest_api.py +++ b/pype/modules/avalon_apps/rest_api.py @@ -4,14 +4,14 @@ import json import bson import bson.json_util from pype.modules.rest_api import RestApi, abort, CallbackResult -from pype.modules.ftrack.lib.custom_db_connector import DbConnector +from pype.modules.ftrack.lib.io_nonsingleton import DbConnector class AvalonRestApi(RestApi): - dbcon = DbConnector(os.environ["AVALON_DB"]) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.dbcon = DbConnector() self.dbcon.install() @RestApi.route("/projects/", url_prefix="/avalon", methods="GET") From 4104397f22f7308fa13ba14605102abf981b561f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:53:24 +0200 Subject: [PATCH 123/131] fixed logging gui variables --- pype/modules/logging/gui/models.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pype/modules/logging/gui/models.py b/pype/modules/logging/gui/models.py index 945b76152b..ce1fa236a9 100644 --- a/pype/modules/logging/gui/models.py +++ b/pype/modules/logging/gui/models.py @@ -1,7 +1,7 @@ import collections from Qt import QtCore from pype.api import Logger -from pypeapp.lib.log import _bootstrap_mongo_log, COLLECTION +from pypeapp.lib.log import _bootstrap_mongo_log, LOG_COLLECTION_NAME log = Logger().get_logger("LogModel", "LoggingModule") @@ -40,10 +40,11 @@ class LogModel(QtCore.QAbstractItemModel): super(LogModel, self).__init__(parent) self._root_node = Node() - database = _bootstrap_mongo_log() self.dbcon = None - if COLLECTION in database.list_collection_names(): - self.dbcon = database[COLLECTION] + # Crash if connection is not possible to skip this module + database = _bootstrap_mongo_log() + if LOG_COLLECTION_NAME in database.list_collection_names(): + self.dbcon = database[LOG_COLLECTION_NAME] def add_log(self, log): node = Node(log) From 9089aaafd71215c3c0aa9c006feb0eb9664431c0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:53:47 +0200 Subject: [PATCH 124/131] event storer connection fixed usage --- pype/modules/ftrack/ftrack_server/sub_event_storer.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pype/modules/ftrack/ftrack_server/sub_event_storer.py b/pype/modules/ftrack/ftrack_server/sub_event_storer.py index 727d5aa515..61b9aaf2c8 100644 --- a/pype/modules/ftrack/ftrack_server/sub_event_storer.py +++ b/pype/modules/ftrack/ftrack_server/sub_event_storer.py @@ -23,11 +23,8 @@ class SessionFactory: session = None -url, database, table_name = get_ftrack_event_mongo_info() -dbcon = DbConnector( - database_name=database, - table_name=table_name -) +uri, port, database, table_name = get_ftrack_event_mongo_info() +dbcon = DbConnector(uri, port, database, table_name) # ignore_topics = ["ftrack.meta.connected"] ignore_topics = [] From c30ba2d86b0ecbe200acb275f38611e304836927 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:55:03 +0200 Subject: [PATCH 125/131] ftrack event may have custom mongo connection --- pype/modules/ftrack/ftrack_server/lib.py | 40 +++++++++++++++++++----- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py index 742976104e..327fab817d 100644 --- a/pype/modules/ftrack/ftrack_server/lib.py +++ b/pype/modules/ftrack/ftrack_server/lib.py @@ -19,7 +19,12 @@ import ftrack_api._centralized_storage_scenario import ftrack_api.event from ftrack_api.logging import LazyLogMessage as L -from pype.api import Logger, compose_url, get_default_components +from pype.api import ( + Logger, + get_default_components, + decompose_url, + compose_url +) from pype.modules.ftrack.lib.custom_db_connector import DbConnector @@ -29,11 +34,28 @@ TOPIC_STATUS_SERVER_RESULT = "pype.event.server.status.result" def get_ftrack_event_mongo_info(): - url = compose_url(get_default_components()) - database = os.environ.get("FTRACK_EVENTS_MONGO_DB") or "pype" - collection = os.environ.get("FTRACK_EVENTS_MONGO_COL") or "ftrack_events" + database_name = ( + os.environ.get("FTRACK_EVENTS_MONGO_DB") or "pype" + ) + collection_name = ( + os.environ.get("FTRACK_EVENTS_MONGO_COL") or "ftrack_events" + ) - return url, database, collection + mongo_url = os.environ.get("FTRACK_EVENTS_MONGO_URL") + if mongo_url is not None: + components = decompose_url(mongo_url) + _used_ftrack_url = True + else: + components = get_default_components() + _used_ftrack_url = False + + if not _used_ftrack_url or components["database"] is None: + components["database"] = database_name + components["collection"] = collection_name + + uri = compose_url(components) + + return uri, components["port"], database_name, collection_name def check_ftrack_url(url, log_errors=True): @@ -137,15 +159,17 @@ class StorerEventHub(SocketBaseEventHub): class ProcessEventHub(SocketBaseEventHub): hearbeat_msg = b"processor" - url, database, table_name = get_ftrack_event_mongo_info() + uri, port, database, table_name = get_ftrack_event_mongo_info() is_table_created = False pypelog = Logger().get_logger("Session Processor") def __init__(self, *args, **kwargs): self.dbcon = DbConnector( - database_name=self.database, - table_name=self.table_name + self.uri, + self.port, + self.database, + self.table_name ) super(ProcessEventHub, self).__init__(*args, **kwargs) From e781888d5adf86ebba0a46d65599c18865d83b0f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:56:09 +0200 Subject: [PATCH 126/131] changed event server cli to can use new access to mongo --- .../ftrack/ftrack_server/event_server_cli.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pype/modules/ftrack/ftrack_server/event_server_cli.py b/pype/modules/ftrack/ftrack_server/event_server_cli.py index 7ca04756df..73c7abfc5d 100644 --- a/pype/modules/ftrack/ftrack_server/event_server_cli.py +++ b/pype/modules/ftrack/ftrack_server/event_server_cli.py @@ -15,8 +15,10 @@ import uuid import ftrack_api import pymongo from pype.modules.ftrack.lib import credentials -from pype.modules.ftrack.ftrack_server.lib import check_ftrack_url -from pype.api import get_default_components, compose_url +from pype.modules.ftrack.ftrack_server.lib import ( + check_ftrack_url, get_ftrack_event_mongo_info +) + import socket_thread @@ -187,9 +189,10 @@ def main_loop(ftrack_url): os.environ["FTRACK_EVENT_SUB_ID"] = str(uuid.uuid1()) # Get mongo hostname and port for testing mongo connection - components = get_default_components() - mongo_port = components.pop("port") - mongo_hostname = compose_url(**components) + + mongo_uri, mongo_port, database_name, collection_name = ( + get_ftrack_event_mongo_info() + ) # Current file file_path = os.path.dirname(os.path.realpath(__file__)) @@ -267,13 +270,12 @@ def main_loop(ftrack_url): ftrack_accessible = check_ftrack_url(ftrack_url) if not mongo_accessible: - mongo_accessible = check_mongo_url(mongo_hostname, mongo_port) + mongo_accessible = check_mongo_url(mongo_uri, mongo_port) # Run threads only if Ftrack is accessible if not ftrack_accessible or not mongo_accessible: if not mongo_accessible and not printed_mongo_error: - mongo_url = "{}:{}".format(mongo_hostname, mongo_port) - print("Can't access Mongo {}".format(mongo_url)) + print("Can't access Mongo {}".format(mongo_uri)) if not ftrack_accessible and not printed_ftrack_error: print("Can't access Ftrack {}".format(ftrack_url)) From cd5859cf30b295723ed53f7c204932740b99e3cd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 16:13:29 +0200 Subject: [PATCH 127/131] add `get_default_components` to __all__ in pype.api --- pype/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/api.py b/pype/api.py index 4b3d50044f..5775bb3ce4 100644 --- a/pype/api.py +++ b/pype/api.py @@ -52,6 +52,7 @@ __all__ = [ "execute", "decompose_url", "compose_url", + "get_default_components", # plugin classes "Extractor", From db4ab28113b5dde2c8f51f5f2787047e9a0fb5c0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 22 Jun 2020 15:21:23 +0100 Subject: [PATCH 128/131] Using existing folders for instances. --- pype/plugins/photoshop/create/create_image.py | 73 ++++++++++++++++++- .../photoshop/publish/validate_naming.py | 47 ++++++++++++ 2 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 pype/plugins/photoshop/publish/validate_naming.py diff --git a/pype/plugins/photoshop/create/create_image.py b/pype/plugins/photoshop/create/create_image.py index a840dd13a7..ff0a5dcb6c 100644 --- a/pype/plugins/photoshop/create/create_image.py +++ b/pype/plugins/photoshop/create/create_image.py @@ -1,12 +1,77 @@ -from avalon import photoshop +from avalon import api, photoshop +from avalon.vendor import Qt -class CreateImage(photoshop.Creator): +class CreateImage(api.Creator): """Image folder for publish.""" name = "imageDefault" label = "Image" family = "image" - def __init__(self, *args, **kwargs): - super(CreateImage, self).__init__(*args, **kwargs) + def process(self): + groups = [] + layers = [] + create_group = False + group_constant = photoshop.get_com_objects().constants().psLayerSet + if (self.options or {}).get("useSelection"): + multiple_instances = False + selection = photoshop.get_selected_layers() + + if len(selection) > 1: + # Ask user whether to create one image or image per selected + # item. + msg_box = Qt.QtWidgets.QMessageBox() + msg_box.setIcon(Qt.QtWidgets.QMessageBox.Warning) + msg_box.setText( + "Multiple layers selected." + "\nDo you want to make one image per layer?" + ) + msg_box.setStandardButtons( + Qt.QtWidgets.QMessageBox.Yes | + Qt.QtWidgets.QMessageBox.No | + Qt.QtWidgets.QMessageBox.Cancel + ) + ret = msg_box.exec_() + if ret == Qt.QtWidgets.QMessageBox.Yes: + multiple_instances = True + elif ret == Qt.QtWidgets.QMessageBox.Cancel: + return + + if multiple_instances: + for item in selection: + if item.LayerType == group_constant: + groups.append(item) + else: + layers.append(item) + else: + group = photoshop.group_selected_layers() + group.Name = self.name + groups.append(group) + + elif len(selection) == 1: + # One selected item. Use group if its a LayerSet (group), else + # create a new group. + if selection[0].LayerType == group_constant: + groups.append(selection[0]) + else: + layers.append(selection[0]) + elif len(selection) == 0: + # No selection creates an empty group. + create_group = True + else: + create_group = True + + if create_group: + group = photoshop.app().ActiveDocument.LayerSets.Add() + group.Name = self.name + groups.append(group) + + for layer in layers: + photoshop.select_layers([layer]) + group = photoshop.group_selected_layers() + group.Name = layer.Name + groups.append(group) + + for group in groups: + photoshop.imprint(group, self.data) diff --git a/pype/plugins/photoshop/publish/validate_naming.py b/pype/plugins/photoshop/publish/validate_naming.py new file mode 100644 index 0000000000..22fa85463c --- /dev/null +++ b/pype/plugins/photoshop/publish/validate_naming.py @@ -0,0 +1,47 @@ +import os + +import pyblish.api +import pype.api +from avalon import photoshop + + +class ValidateNamingRepair(pyblish.api.Action): + """Repair the instance asset.""" + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + + # Get the errored instances + failed = [] + for result in context.data["results"]: + if (result["error"] is not None and result["instance"] is not None + and result["instance"] not in failed): + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + + for instance in instances: + instance[0].Name = instance.data["name"].replace(" ", "_") + + return True + + +class ValidateNaming(pyblish.api.InstancePlugin): + """Validate the instance name. + + Spaces in names are not allowed. Will be replace with underscores. + """ + + label = "Validate Naming" + hosts = ["photoshop"] + order = pype.api.ValidateContentsOrder + families = ["image"] + actions = [ValidateNamingRepair] + + def process(self, instance): + msg = "Name \"{}\" is not allowed.".format(instance.data["name"]) + assert " " not in instance.data["name"], msg From f87da7168c77ed8d151c80fd8b3cbd9cc3539ccf Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 22 Jun 2020 15:29:06 +0100 Subject: [PATCH 129/131] Hound --- pype/plugins/photoshop/publish/validate_naming.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pype/plugins/photoshop/publish/validate_naming.py b/pype/plugins/photoshop/publish/validate_naming.py index 22fa85463c..1d85ea99a0 100644 --- a/pype/plugins/photoshop/publish/validate_naming.py +++ b/pype/plugins/photoshop/publish/validate_naming.py @@ -1,8 +1,5 @@ -import os - import pyblish.api import pype.api -from avalon import photoshop class ValidateNamingRepair(pyblish.api.Action): From 8ece0dc8fe516712af04fd603bdea739f7e652a5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 23 Jun 2020 11:50:11 +0200 Subject: [PATCH 130/131] changed check of "error" of result after action --- pype/tools/pyblish_pype/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 2967488572..3c7808496c 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -984,7 +984,7 @@ class Window(QtWidgets.QDialog): self._suspend_logs ) - if "error" in result: + if result.get("error"): action_state |= PluginActionStates.HasFailed plugin_item.setData(action_state, Roles.PluginActionProgressRole) From a375a6e29bd1c679bb48b008dbd392c069539b8d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 23 Jun 2020 15:49:24 +0200 Subject: [PATCH 131/131] bump version --- pype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/version.py b/pype/version.py index 334087f851..1c622223ba 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.9.1" +__version__ = "2.10.0"