[Automated] Merged develop into main

This commit is contained in:
pypebot 2022-12-07 04:29:42 +01:00 committed by GitHub
commit 99fd1c2fe6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1064 additions and 472 deletions

View file

@ -0,0 +1,10 @@
from .addon import (
CELACTION_ROOT_DIR,
CelactionAddon,
)
__all__ = (
"CELACTION_ROOT_DIR",
"CelactionAddon",
)

View file

@ -0,0 +1,31 @@
import os
from openpype.modules import OpenPypeModule, IHostAddon
CELACTION_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
class CelactionAddon(OpenPypeModule, IHostAddon):
name = "celaction"
host_name = "celaction"
def initialize(self, module_settings):
self.enabled = True
def get_launch_hook_paths(self, app):
if app.host_name != self.host_name:
return []
return [
os.path.join(CELACTION_ROOT_DIR, "hooks")
]
def add_implementation_envs(self, env, _app):
# Set default values if are not already set via settings
defaults = {
"LOGLEVEL": "DEBUG"
}
for key, value in defaults.items():
if not env.get(key):
env[key] = value
def get_workfile_extensions(self):
return [".scn"]

View file

@ -1 +0,0 @@
kwargs = None

View file

@ -1,87 +0,0 @@
import os
import sys
import copy
import argparse
import pyblish.api
import pyblish.util
import openpype.hosts.celaction
from openpype.lib import Logger
from openpype.hosts.celaction import api as celaction
from openpype.tools.utils import host_tools
from openpype.pipeline import install_openpype_plugins
log = Logger.get_logger("Celaction_cli_publisher")
publish_host = "celaction"
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.celaction.__file__))
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "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"))
celaction.kwargs = parser.parse_args(sys.argv[1:]).__dict__
def _prepare_publish_environments():
"""Prepares environments based on request data."""
env = copy.deepcopy(os.environ)
project_name = os.getenv("AVALON_PROJECT")
asset_name = os.getenv("AVALON_ASSET")
env["AVALON_PROJECT"] = project_name
env["AVALON_ASSET"] = asset_name
env["AVALON_TASK"] = os.getenv("AVALON_TASK")
env["AVALON_WORKDIR"] = os.getenv("AVALON_WORKDIR")
env["AVALON_APP"] = f"hosts.{publish_host}"
env["AVALON_APP_NAME"] = "celaction/local"
env["PYBLISH_HOSTS"] = publish_host
os.environ.update(env)
def main():
# prepare all environments
_prepare_publish_environments()
# Registers pype's Global pyblish plugins
install_openpype_plugins()
if os.path.exists(PUBLISH_PATH):
log.info(f"Registering path: {PUBLISH_PATH}")
pyblish.api.register_plugin_path(PUBLISH_PATH)
pyblish.api.register_host(publish_host)
return host_tools.show_publish()
if __name__ == "__main__":
cli()
result = main()
sys.exit(not bool(result))

View file

@ -1,122 +0,0 @@
import os
import shutil
import winreg
from openpype.lib import PreLaunchHook
from openpype.hosts.celaction import api as celaction
class CelactionPrelaunchHook(PreLaunchHook):
"""
Bootstrap celacion with pype
"""
workfile_ext = "scn"
app_groups = ["celaction"]
platforms = ["windows"]
def execute(self):
# Add workfile path to launch arguments
workfile_path = self.workfile_path()
if workfile_path:
self.launch_context.launch_args.append(workfile_path)
project_name = self.data["project_name"]
asset_name = self.data["asset_name"]
task_name = self.data["task_name"]
# get publish version of celaction
app = "celaction_publish"
# setting output parameters
path = r"Software\CelAction\CelAction2D\User Settings"
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: this will need to be checked more thoroughly
pype_exe = os.getenv("OPENPYPE_EXECUTABLE")
winreg.SetValueEx(hKey, "SubmitAppTitle", 0, winreg.REG_SZ, pype_exe)
parameters = [
"launch",
f"--app {app}",
f"--project {project_name}",
f"--asset {asset_name}",
f"--task {task_name}",
"--currentFile \\\"\"*SCENE*\"\\\"",
"--chunk 10",
"--frameStart *START*",
"--frameEnd *END*",
"--resolutionWidth *X*",
"--resolutionHeight *Y*",
# "--programDir \"'*PROGPATH*'\""
]
winreg.SetValueEx(hKey, "SubmitParametersTitle", 0, winreg.REG_SZ,
" ".join(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)
def workfile_path(self):
workfile_path = self.data["last_workfile_path"]
# copy workfile from template if doesnt exist any on path
if not os.path.exists(workfile_path):
# TODO add ability to set different template workfile path via
# settings
pype_celaction_dir = os.path.dirname(os.path.dirname(
os.path.abspath(celaction.__file__)
))
template_path = os.path.join(
pype_celaction_dir,
"resources",
"celaction_template_scene.scn"
)
if not os.path.exists(template_path):
self.log.warning(
"Couldn't find workfile template file in {}".format(
template_path
)
)
return
self.log.info(
f"Creating workfile from template: \"{template_path}\""
)
# Copy template workfile to new destinantion
shutil.copy2(
os.path.normpath(template_path),
os.path.normpath(workfile_path)
)
self.log.info(f"Workfile to open: \"{workfile_path}\"")
return workfile_path

View file

@ -0,0 +1,137 @@
import os
import shutil
import winreg
import subprocess
from openpype.lib import PreLaunchHook, get_openpype_execute_args
from openpype.hosts.celaction import scripts
CELACTION_SCRIPTS_DIR = os.path.dirname(
os.path.abspath(scripts.__file__)
)
class CelactionPrelaunchHook(PreLaunchHook):
"""
Bootstrap celacion with pype
"""
app_groups = ["celaction"]
platforms = ["windows"]
def execute(self):
asset_doc = self.data["asset_doc"]
width = asset_doc["data"]["resolutionWidth"]
height = asset_doc["data"]["resolutionHeight"]
# Add workfile path to launch arguments
workfile_path = self.workfile_path()
if workfile_path:
self.launch_context.launch_args.append(workfile_path)
# setting output parameters
path_user_settings = "\\".join([
"Software", "CelAction", "CelAction2D", "User Settings"
])
winreg.CreateKey(winreg.HKEY_CURRENT_USER, path_user_settings)
hKey = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, path_user_settings, 0,
winreg.KEY_ALL_ACCESS
)
path_to_cli = os.path.join(CELACTION_SCRIPTS_DIR, "publish_cli.py")
subproces_args = get_openpype_execute_args("run", path_to_cli)
openpype_executable = subproces_args.pop(0)
winreg.SetValueEx(
hKey,
"SubmitAppTitle",
0,
winreg.REG_SZ,
openpype_executable
)
parameters = subproces_args + [
"--currentFile", "*SCENE*",
"--chunk", "*CHUNK*",
"--frameStart", "*START*",
"--frameEnd", "*END*",
"--resolutionWidth", "*X*",
"--resolutionHeight", "*Y*"
]
winreg.SetValueEx(
hKey, "SubmitParametersTitle", 0, winreg.REG_SZ,
subprocess.list2cmdline(parameters)
)
# setting resolution parameters
path_submit = "\\".join([
path_user_settings, "Dialogs", "SubmitOutput"
])
winreg.CreateKey(winreg.HKEY_CURRENT_USER, path_submit)
hKey = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, path_submit, 0,
winreg.KEY_ALL_ACCESS
)
winreg.SetValueEx(hKey, "SaveScene", 0, winreg.REG_DWORD, 1)
winreg.SetValueEx(hKey, "CustomX", 0, winreg.REG_DWORD, width)
winreg.SetValueEx(hKey, "CustomY", 0, winreg.REG_DWORD, height)
# making sure message dialogs don't appear when overwriting
path_overwrite_scene = "\\".join([
path_user_settings, "Messages", "OverwriteScene"
])
winreg.CreateKey(winreg.HKEY_CURRENT_USER, path_overwrite_scene)
hKey = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, path_overwrite_scene, 0,
winreg.KEY_ALL_ACCESS
)
winreg.SetValueEx(hKey, "Result", 0, winreg.REG_DWORD, 6)
winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1)
# set scane as not saved
path_scene_saved = "\\".join([
path_user_settings, "Messages", "SceneSaved"
])
winreg.CreateKey(winreg.HKEY_CURRENT_USER, path_scene_saved)
hKey = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, path_scene_saved, 0,
winreg.KEY_ALL_ACCESS
)
winreg.SetValueEx(hKey, "Result", 0, winreg.REG_DWORD, 1)
winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1)
def workfile_path(self):
workfile_path = self.data["last_workfile_path"]
# copy workfile from template if doesnt exist any on path
if not os.path.exists(workfile_path):
# TODO add ability to set different template workfile path via
# settings
openpype_celaction_dir = os.path.dirname(CELACTION_SCRIPTS_DIR)
template_path = os.path.join(
openpype_celaction_dir,
"resources",
"celaction_template_scene.scn"
)
if not os.path.exists(template_path):
self.log.warning(
"Couldn't find workfile template file in {}".format(
template_path
)
)
return
self.log.info(
f"Creating workfile from template: \"{template_path}\""
)
# Copy template workfile to new destinantion
shutil.copy2(
os.path.normpath(template_path),
os.path.normpath(workfile_path)
)
self.log.info(f"Workfile to open: \"{workfile_path}\"")
return workfile_path

View file

@ -1,5 +1,7 @@
import pyblish.api
from openpype.hosts.celaction import api as celaction
import argparse
import sys
from pprint import pformat
class CollectCelactionCliKwargs(pyblish.api.Collector):
@ -9,15 +11,31 @@ class CollectCelactionCliKwargs(pyblish.api.Collector):
order = pyblish.api.Collector.order - 0.1
def process(self, context):
kwargs = celaction.kwargs.copy()
parser = argparse.ArgumentParser(prog="celaction")
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"))
passing_kwargs = parser.parse_args(sys.argv[1:]).__dict__
self.log.info("Storing kwargs: %s" % kwargs)
context.set_data("kwargs", kwargs)
self.log.info("Storing kwargs ...")
self.log.debug("_ passing_kwargs: {}".format(pformat(passing_kwargs)))
# set kwargs to context data
context.set_data("passingKwargs", passing_kwargs)
# get kwargs onto context data as keys with values
for k, v in kwargs.items():
for k, v in passing_kwargs.items():
self.log.info(f"Setting `{k}` to instance.data with value: `{v}`")
if k in ["frameStart", "frameEnd"]:
context.data[k] = kwargs[k] = int(v)
context.data[k] = passing_kwargs[k] = int(v)
else:
context.data[k] = v

View file

@ -36,7 +36,8 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin):
"version": version
}
celaction_kwargs = context.data.get("kwargs", {})
celaction_kwargs = context.data.get(
"passingKwargs", {})
if celaction_kwargs:
shared_instance_data.update(celaction_kwargs)
@ -52,8 +53,8 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin):
"subset": subset,
"label": scene_file,
"family": family,
"families": [family, "ftrack"],
"representations": list()
"families": [],
"representations": []
})
# adding basic script data
@ -72,7 +73,6 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin):
self.log.info('Publishing Celaction workfile')
# render instance
family = "render.farm"
subset = f"render{task}Main"
instance = context.create_instance(name=subset)
# getting instance state
@ -81,8 +81,8 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin):
# add assetEntity data into instance
instance.data.update({
"label": "{} - farm".format(subset),
"family": family,
"families": [family],
"family": "render.farm",
"families": [],
"subset": subset
})

View file

@ -11,28 +11,31 @@ class CollectRenderPath(pyblish.api.InstancePlugin):
families = ["render.farm"]
# Presets
anatomy_render_key = None
publish_render_metadata = None
output_extension = "png"
anatomy_template_key_render_files = None
anatomy_template_key_metadata = None
def process(self, instance):
anatomy = instance.context.data["anatomy"]
anatomy_data = copy.deepcopy(instance.data["anatomyData"])
anatomy_data["family"] = "render"
padding = anatomy.templates.get("frame_padding", 4)
anatomy_data.update({
"frame": f"%0{padding}d",
"representation": "png"
"family": "render",
"representation": self.output_extension,
"ext": self.output_extension
})
anatomy_filled = anatomy.format(anatomy_data)
# get anatomy rendering keys
anatomy_render_key = self.anatomy_render_key or "render"
publish_render_metadata = self.publish_render_metadata or "render"
r_anatomy_key = self.anatomy_template_key_render_files
m_anatomy_key = self.anatomy_template_key_metadata
# get folder and path for rendering images from celaction
render_dir = anatomy_filled[anatomy_render_key]["folder"]
render_path = anatomy_filled[anatomy_render_key]["path"]
render_dir = anatomy_filled[r_anatomy_key]["folder"]
render_path = anatomy_filled[r_anatomy_key]["path"]
self.log.debug("__ render_path: `{}`".format(render_path))
# create dir if it doesnt exists
try:
@ -46,9 +49,9 @@ class CollectRenderPath(pyblish.api.InstancePlugin):
instance.data["path"] = render_path
# get anatomy for published renders folder path
if anatomy_filled.get(publish_render_metadata):
if anatomy_filled.get(m_anatomy_key):
instance.data["publishRenderMetadataFolder"] = anatomy_filled[
publish_render_metadata]["folder"]
m_anatomy_key]["folder"]
self.log.info("Metadata render path: `{}`".format(
instance.data["publishRenderMetadataFolder"]
))

View file

@ -0,0 +1,37 @@
import os
import sys
import pyblish.api
import pyblish.util
import openpype.hosts.celaction
from openpype.lib import Logger
from openpype.tools.utils import host_tools
from openpype.pipeline import install_openpype_plugins
log = Logger.get_logger("celaction")
PUBLISH_HOST = "celaction"
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.celaction.__file__))
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
def main():
# Registers pype's Global pyblish plugins
install_openpype_plugins()
if os.path.exists(PUBLISH_PATH):
log.info(f"Registering path: {PUBLISH_PATH}")
pyblish.api.register_plugin_path(PUBLISH_PATH)
pyblish.api.register_host(PUBLISH_HOST)
pyblish.api.register_target("local")
return host_tools.show_publish()
if __name__ == "__main__":
result = main()
sys.exit(not bool(result))

View file

@ -596,18 +596,28 @@ class PublishableClip:
if not hero_track and self.vertical_sync:
# driving layer is set as negative match
for (_in, _out), hero_data in self.vertical_clip_match.items():
hero_data.update({"heroTrack": False})
if _in == self.clip_in and _out == self.clip_out:
"""
Since only one instance of hero clip is expected in
`self.vertical_clip_match`, this will loop only once
until none hero clip will be matched with hero clip.
`tag_hierarchy_data` will be set only once for every
clip which is not hero clip.
"""
_hero_data = deepcopy(hero_data)
_hero_data.update({"heroTrack": False})
if _in <= self.clip_in and _out >= self.clip_out:
data_subset = hero_data["subset"]
# add track index in case duplicity of names in hero data
if self.subset in data_subset:
hero_data["subset"] = self.subset + str(
_hero_data["subset"] = self.subset + str(
self.track_index)
# in case track name and subset name is the same then add
if self.subset_name == self.track_name:
hero_data["subset"] = self.subset
_hero_data["subset"] = self.subset
# assing data to return hierarchy data to tag
tag_hierarchy_data = hero_data
tag_hierarchy_data = _hero_data
break
# add data to return data dict
self.marker_data.update(tag_hierarchy_data)

View file

@ -27,7 +27,12 @@ class HieroAddon(OpenPypeModule, IHostAddon):
new_hiero_paths.append(norm_path)
env["HIERO_PLUGIN_PATH"] = os.pathsep.join(new_hiero_paths)
# Remove auto screen scale factor for Qt
# - let Hiero decide it's value
env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None)
# Remove tkinter library paths if are set
env.pop("TK_LIBRARY", None)
env.pop("TCL_LIBRARY", None)
# Add vendor to PYTHONPATH
python_path = env["PYTHONPATH"]

View file

@ -27,7 +27,12 @@ class NukeAddon(OpenPypeModule, IHostAddon):
new_nuke_paths.append(norm_path)
env["NUKE_PATH"] = os.pathsep.join(new_nuke_paths)
# Remove auto screen scale factor for Qt
# - let Nuke decide it's value
env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None)
# Remove tkinter library paths if are set
env.pop("TK_LIBRARY", None)
env.pop("TCL_LIBRARY", None)
# Add vendor to PYTHONPATH
python_path = env["PYTHONPATH"]

View file

@ -2961,7 +2961,7 @@ def get_viewer_config_from_string(input_string):
viewer = split[1]
display = split[0]
elif "(" in viewer:
pattern = r"([\w\d\s]+).*[(](.*)[)]"
pattern = r"([\w\d\s\.\-]+).*[(](.*)[)]"
result = re.findall(pattern, viewer)
try:
result = result.pop()

View file

@ -298,7 +298,7 @@ class ExtractSlateFrame(publish.Extractor):
def add_comment_slate_node(self, instance, node):
comment = instance.context.data.get("comment")
comment = instance.data["comment"]
intent = instance.context.data.get("intent")
if not isinstance(intent, dict):
intent = {

View file

@ -1,5 +1,7 @@
import os
import pyblish.api
from openpype.settings import get_project_settings
from openpype.pipeline.publish import (
ValidateContentsOrder,
PublishXmlValidationError,
@ -18,23 +20,38 @@ class ValidateTextureBatchWorkfiles(pyblish.api.InstancePlugin):
families = ["texture_batch_workfile"]
optional = True
# from presets
main_workfile_extensions = ['mra']
def process(self, instance):
if instance.data["family"] == "workfile":
ext = instance.data["representations"][0]["ext"]
if ext not in self.main_workfile_extensions:
main_workfile_extensions = self.get_main_workfile_extensions()
if ext not in main_workfile_extensions:
self.log.warning("Only secondary workfile present!")
return
if not instance.data.get("resources"):
msg = "No secondary workfile present for workfile '{}'". \
format(instance.data["name"])
ext = self.main_workfile_extensions[0]
ext = main_workfile_extensions[0]
formatting_data = {"file_name": instance.data["name"],
"extension": ext}
raise PublishXmlValidationError(self, msg,
formatting_data=formatting_data
)
@staticmethod
def get_main_workfile_extensions():
project_settings = get_project_settings(os.environ["AVALON_PROJECT"])
try:
extensions = (project_settings["standalonepublisher"]
["publish"]
["CollectTextures"]
["main_workfile_extensions"])
except KeyError:
raise Exception("Setting 'Main workfile extensions' not found."
" The setting must be set for the"
" 'Collect Texture' publish plugin of the"
" 'Standalone Publish' tool.")
return extensions

View file

@ -14,9 +14,9 @@ else:
class FileTransaction(object):
"""
"""File transaction with rollback options.
The file transaction is a three step process.
The file transaction is a three-step process.
1) Rename any existing files to a "temporary backup" during `process()`
2) Copy the files to final destination during `process()`
@ -39,14 +39,12 @@ class FileTransaction(object):
Warning:
Any folders created during the transfer will not be removed.
"""
MODE_COPY = 0
MODE_HARDLINK = 1
def __init__(self, log=None):
if log is None:
log = logging.getLogger("FileTransaction")
@ -63,49 +61,64 @@ class FileTransaction(object):
self._backup_to_original = {}
def add(self, src, dst, mode=MODE_COPY):
"""Add a new file to transfer queue"""
"""Add a new file to transfer queue.
Args:
src (str): Source path.
dst (str): Destination path.
mode (MODE_COPY, MODE_HARDLINK): Transfer mode.
"""
opts = {"mode": mode}
src = os.path.abspath(src)
dst = os.path.abspath(dst)
src = os.path.normpath(os.path.abspath(src))
dst = os.path.normpath(os.path.abspath(dst))
if dst in self._transfers:
queued_src = self._transfers[dst][0]
if src == queued_src:
self.log.debug("File transfer was already "
"in queue: {} -> {}".format(src, dst))
self.log.debug(
"File transfer was already in queue: {} -> {}".format(
src, dst))
return
else:
self.log.warning("File transfer in queue replaced..")
self.log.debug("Removed from queue: "
"{} -> {}".format(queued_src, dst))
self.log.debug("Added to queue: {} -> {}".format(src, dst))
self.log.debug(
"Removed from queue: {} -> {} replaced by {} -> {}".format(
queued_src, dst, src, dst))
self._transfers[dst] = (src, opts)
def process(self):
# Backup any existing files
for dst in self._transfers.keys():
if os.path.exists(dst):
# Backup original file
# todo: add timestamp or uuid to ensure unique
backup = dst + ".bak"
self._backup_to_original[backup] = dst
self.log.debug("Backup existing file: "
"{} -> {}".format(dst, backup))
os.rename(dst, backup)
for dst, (src, _) in self._transfers.items():
if dst == src or not os.path.exists(dst):
continue
# Backup original file
# todo: add timestamp or uuid to ensure unique
backup = dst + ".bak"
self._backup_to_original[backup] = dst
self.log.debug(
"Backup existing file: {} -> {}".format(dst, backup))
os.rename(dst, backup)
# Copy the files to transfer
for dst, (src, opts) in self._transfers.items():
if dst == src:
self.log.debug(
"Source and destionation are same files {} -> {}".format(
src, dst))
continue
self._create_folder_for_file(dst)
if opts["mode"] == self.MODE_COPY:
self.log.debug("Copying file ... {} -> {}".format(src, dst))
copyfile(src, dst)
elif opts["mode"] == self.MODE_HARDLINK:
self.log.debug("Hardlinking file ... {} -> {}".format(src,
dst))
self.log.debug("Hardlinking file ... {} -> {}".format(
src, dst))
create_hard_link(src, dst)
self._transferred.append(dst)
@ -116,23 +129,21 @@ class FileTransaction(object):
try:
os.remove(backup)
except OSError:
self.log.error("Failed to remove backup file: "
"{}".format(backup),
exc_info=True)
self.log.error(
"Failed to remove backup file: {}".format(backup),
exc_info=True)
def rollback(self):
errors = 0
# Rollback any transferred files
for path in self._transferred:
try:
os.remove(path)
except OSError:
errors += 1
self.log.error("Failed to rollback created file: "
"{}".format(path),
exc_info=True)
self.log.error(
"Failed to rollback created file: {}".format(path),
exc_info=True)
# Rollback the backups
for backup, original in self._backup_to_original.items():
@ -140,13 +151,15 @@ class FileTransaction(object):
os.rename(backup, original)
except OSError:
errors += 1
self.log.error("Failed to restore original file: "
"{} -> {}".format(backup, original),
exc_info=True)
self.log.error(
"Failed to restore original file: {} -> {}".format(
backup, original),
exc_info=True)
if errors:
self.log.error("{} errors occurred during "
"rollback.".format(errors), exc_info=True)
self.log.error(
"{} errors occurred during rollback.".format(errors),
exc_info=True)
six.reraise(*sys.exc_info())
@property

View file

@ -77,26 +77,38 @@ def get_transcode_temp_directory():
)
def get_oiio_info_for_input(filepath, logger=None):
def get_oiio_info_for_input(filepath, logger=None, subimages=False):
"""Call oiiotool to get information about input and return stdout.
Stdout should contain xml format string.
"""
args = [
get_oiio_tools_path(), "--info", "-v", "-i:infoformat=xml", filepath
get_oiio_tools_path(),
"--info",
"-v"
]
if subimages:
args.append("-a")
args.extend(["-i:infoformat=xml", filepath])
output = run_subprocess(args, logger=logger)
output = output.replace("\r\n", "\n")
xml_started = False
subimages_lines = []
lines = []
for line in output.split("\n"):
if not xml_started:
if not line.startswith("<"):
continue
xml_started = True
if xml_started:
lines.append(line)
if line == "</ImageSpec>":
subimages_lines.append(lines)
lines = []
if not xml_started:
raise ValueError(
@ -105,12 +117,19 @@ def get_oiio_info_for_input(filepath, logger=None):
)
)
xml_text = "\n".join(lines)
return parse_oiio_xml_output(xml_text, logger=logger)
output = []
for subimage_lines in subimages_lines:
xml_text = "\n".join(subimage_lines)
output.append(parse_oiio_xml_output(xml_text, logger=logger))
if subimages:
return output
return output[0]
class RationalToInt:
"""Rational value stored as division of 2 integers using string."""
def __init__(self, string_value):
parts = string_value.split("/")
top = float(parts[0])
@ -157,16 +176,16 @@ def convert_value_by_type_name(value_type, value, logger=None):
if value_type == "int":
return int(value)
if value_type == "float":
if value_type in ("float", "double"):
return float(value)
# Vectors will probably have more types
if value_type in ("vec2f", "float2"):
if value_type in ("vec2f", "float2", "float2d"):
return [float(item) for item in value.split(",")]
# Matrix should be always have square size of element 3x3, 4x4
# - are returned as list of lists
if value_type == "matrix":
if value_type in ("matrix", "matrixd"):
output = []
current_index = -1
parts = value.split(",")
@ -198,7 +217,7 @@ def convert_value_by_type_name(value_type, value, logger=None):
if value_type == "rational2i":
return RationalToInt(value)
if value_type == "vector":
if value_type in ("vector", "vectord"):
parts = [part.strip() for part in value.split(",")]
output = []
for part in parts:
@ -380,6 +399,10 @@ def should_convert_for_ffmpeg(src_filepath):
if not input_info:
return None
subimages = input_info.get("subimages")
if subimages is not None and subimages > 1:
return True
# Check compression
compression = input_info["attribs"].get("compression")
if compression in ("dwaa", "dwab"):
@ -453,7 +476,7 @@ def convert_for_ffmpeg(
if input_frame_start is not None and input_frame_end is not None:
is_sequence = int(input_frame_end) != int(input_frame_start)
input_info = get_oiio_info_for_input(first_input_path)
input_info = get_oiio_info_for_input(first_input_path, logger=logger)
# Change compression only if source compression is "dwaa" or "dwab"
# - they're not supported in ffmpeg
@ -488,13 +511,21 @@ def convert_for_ffmpeg(
input_channels.append(alpha)
input_channels_str = ",".join(input_channels)
oiio_cmd.extend([
subimages = input_info.get("subimages")
input_arg = "-i"
if subimages is None or subimages == 1:
# Tell oiiotool which channels should be loaded
# - other channels are not loaded to memory so helps to avoid memory
# leak issues
"-i:ch={}".format(input_channels_str), first_input_path,
# - this option is crashing if used on multipart/subimages exrs
input_arg += ":ch={}".format(input_channels_str)
oiio_cmd.extend([
input_arg, first_input_path,
# Tell oiiotool which channels should be put to top stack (and output)
"--ch", channels_arg
"--ch", channels_arg,
# Use first subimage
"--subimage", "0"
])
# Add frame definitions to arguments
@ -588,7 +619,7 @@ def convert_input_paths_for_ffmpeg(
" \".exr\" extension. Got \"{}\"."
).format(ext))
input_info = get_oiio_info_for_input(first_input_path)
input_info = get_oiio_info_for_input(first_input_path, logger=logger)
# Change compression only if source compression is "dwaa" or "dwab"
# - they're not supported in ffmpeg
@ -606,12 +637,22 @@ def convert_input_paths_for_ffmpeg(
red, green, blue, alpha = review_channels
input_channels = [red, green, blue]
# TODO find subimage inder where rgba is available for multipart exrs
channels_arg = "R={},G={},B={}".format(red, green, blue)
if alpha is not None:
channels_arg += ",A={}".format(alpha)
input_channels.append(alpha)
input_channels_str = ",".join(input_channels)
subimages = input_info.get("subimages")
input_arg = "-i"
if subimages is None or subimages == 1:
# Tell oiiotool which channels should be loaded
# - other channels are not loaded to memory so helps to avoid memory
# leak issues
# - this option is crashing if used on multipart exrs
input_arg += ":ch={}".format(input_channels_str)
for input_path in input_paths:
# Prepare subprocess arguments
oiio_cmd = [
@ -625,13 +666,12 @@ def convert_input_paths_for_ffmpeg(
oiio_cmd.extend(["--compression", compression])
oiio_cmd.extend([
# Tell oiiotool which channels should be loaded
# - other channels are not loaded to memory so helps to
# avoid memory leak issues
"-i:ch={}".format(input_channels_str), input_path,
input_arg, input_path,
# Tell oiiotool which channels should be put to top stack
# (and output)
"--ch", channels_arg
"--ch", channels_arg,
# Use first subimage
"--subimage", "0"
])
for attr_name, attr_value in input_info["attribs"].items():

View file

@ -2,16 +2,14 @@ import os
import re
import json
import getpass
import requests
import pyblish.api
class ExtractCelactionDeadline(pyblish.api.InstancePlugin):
class CelactionSubmitDeadline(pyblish.api.InstancePlugin):
"""Submit CelAction2D scene to Deadline
Renders are submitted to a Deadline Web Service as
supplied via settings key "DEADLINE_REST_URL".
Renders are submitted to a Deadline Web Service.
"""
@ -26,27 +24,21 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin):
deadline_pool_secondary = ""
deadline_group = ""
deadline_chunk_size = 1
enviro_filter = [
"FTRACK_API_USER",
"FTRACK_API_KEY",
"FTRACK_SERVER"
]
deadline_job_delay = "00:00:08:00"
def process(self, instance):
instance.data["toBeRenderedOn"] = "deadline"
context = instance.context
deadline_url = (
context.data["system_settings"]
["modules"]
["deadline"]
["DEADLINE_REST_URL"]
)
assert deadline_url, "Requires DEADLINE_REST_URL"
# get default deadline webservice url from deadline module
deadline_url = instance.context.data["defaultDeadline"]
# if custom one is set in instance, use that
if instance.data.get("deadlineUrl"):
deadline_url = instance.data.get("deadlineUrl")
assert deadline_url, "Requires Deadline Webservice URL"
self.deadline_url = "{}/api/jobs".format(deadline_url)
self._comment = context.data.get("comment", "")
self._comment = instance.data["comment"]
self._deadline_user = context.data.get(
"deadlineUser", getpass.getuser())
self._frame_start = int(instance.data["frameStart"])
@ -82,6 +74,26 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin):
render_dir = os.path.normpath(os.path.dirname(render_path))
render_path = os.path.normpath(render_path)
script_name = os.path.basename(script_path)
for item in instance.context:
if "workfile" in item.data["family"]:
msg = "Workfile (scene) must be published along"
assert item.data["publish"] is True, msg
template_data = item.data.get("anatomyData")
rep = item.data.get("representations")[0].get("name")
template_data["representation"] = rep
template_data["ext"] = rep
template_data["comment"] = None
anatomy_filled = instance.context.data["anatomy"].format(
template_data)
template_filled = anatomy_filled["publish"]["path"]
script_path = os.path.normpath(template_filled)
self.log.info(
"Using published scene for render {}".format(script_path)
)
jobname = "%s - %s" % (script_name, instance.name)
output_filename_0 = self.preview_fname(render_path)
@ -98,7 +110,7 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin):
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()
search_results = re.search(r"(%0)(\d)(d)[._]", render_path).groups()
split_patern = "".join(search_results)
padding_number = int(search_results[1])
@ -145,10 +157,11 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin):
# frames from Deadline Monitor
"OutputFilename0": output_filename_0.replace("\\", "/"),
# # Asset dependency to wait for at least the scene file to sync.
# # Asset dependency to wait for at least
# the scene file to sync.
# "AssetDependency0": script_path
"ScheduledType": "Once",
"JobDelay": "00:00:08:00"
"JobDelay": self.deadline_job_delay
},
"PluginInfo": {
# Input
@ -173,19 +186,6 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin):
plugin = payload["JobInfo"]["Plugin"]
self.log.info("using render plugin : {}".format(plugin))
i = 0
for key, values in dict(os.environ).items():
if key.upper() in self.enviro_filter:
payload["JobInfo"].update(
{
"EnvironmentKeyValue%d"
% i: "{key}={value}".format(
key=key, value=values
)
}
)
i += 1
self.log.info("Submitting..")
self.log.info(json.dumps(payload, indent=4, sort_keys=True))
@ -193,10 +193,15 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin):
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)
self.log.error(
"Submission failed! [{}] {}".format(
response.status_code, response.content))
self.log.debug(payload)
raise SystemExit(response.text)
return response
@ -234,32 +239,29 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin):
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
else:
return path
def expected_files(self,
instance,
path):
self.log.debug("_ path: `{}`".format(path))
return path
def expected_files(self, instance, filepath):
""" Create expected files in instance data
"""
if not instance.data.get("expectedFiles"):
instance.data["expectedFiles"] = list()
instance.data["expectedFiles"] = []
dir = os.path.dirname(path)
file = os.path.basename(path)
dirpath = os.path.dirname(filepath)
filename = os.path.basename(filepath)
if "#" in file:
pparts = file.split("#")
if "#" in filename:
pparts = filename.split("#")
padding = "%0{}d".format(len(pparts) - 1)
file = pparts[0] + padding + pparts[-1]
filename = pparts[0] + padding + pparts[-1]
if "%" not in file:
instance.data["expectedFiles"].append(path)
if "%" not in filename:
instance.data["expectedFiles"].append(filepath)
return
for i in range(self._frame_start, (self._frame_end + 1)):
instance.data["expectedFiles"].append(
os.path.join(dir, (file % i)).replace("\\", "/"))
os.path.join(dirpath, (filename % i)).replace("\\", "/")
)

View file

@ -241,6 +241,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
environment["AVALON_ASSET"] = legacy_io.Session["AVALON_ASSET"]
environment["AVALON_TASK"] = legacy_io.Session["AVALON_TASK"]
environment["AVALON_APP_NAME"] = os.environ.get("AVALON_APP_NAME")
environment["OPENPYPE_VERSION"] = os.environ.get("OPENPYPE_VERSION")
environment["OPENPYPE_LOG_NO_COLORS"] = "1"
environment["OPENPYPE_USERNAME"] = instance.context.data["user"]
environment["OPENPYPE_PUBLISH_JOB"] = "1"
@ -776,6 +777,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
"handleEnd": handle_end,
"frameStartHandle": start - handle_start,
"frameEndHandle": end + handle_end,
"comment": instance.data["comment"],
"fps": fps,
"source": source,
"extendFrames": data.get("extendFrames"),

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View file

@ -0,0 +1,38 @@
[About]
Type=label
Label=About
Category=About Plugin
CategoryOrder=-1
Index=0
Default=Celaction Plugin for Deadline
Description=Not configurable
[ConcurrentTasks]
Type=label
Label=ConcurrentTasks
Category=About Plugin
CategoryOrder=-1
Index=0
Default=True
Description=Not configurable
[Executable]
Type=filename
Label=Executable
Category=Config
CategoryOrder=0
CategoryIndex=0
Description=The command executable to run
Required=false
DisableIfBlank=true
[RenderNameSeparator]
Type=string
Label=RenderNameSeparator
Category=Config
CategoryOrder=0
CategoryIndex=1
Description=The separator to use for naming
Required=false
DisableIfBlank=true
Default=.

View file

@ -0,0 +1,122 @@
from System.Text.RegularExpressions import *
from Deadline.Plugins import *
from Deadline.Scripting import *
import _winreg
######################################################################
# This is the function that Deadline calls to get an instance of the
# main DeadlinePlugin class.
######################################################################
def GetDeadlinePlugin():
return CelActionPlugin()
def CleanupDeadlinePlugin(deadlinePlugin):
deadlinePlugin.Cleanup()
######################################################################
# This is the main DeadlinePlugin class for the CelAction plugin.
######################################################################
class CelActionPlugin(DeadlinePlugin):
def __init__(self):
self.InitializeProcessCallback += self.InitializeProcess
self.RenderExecutableCallback += self.RenderExecutable
self.RenderArgumentCallback += self.RenderArgument
self.StartupDirectoryCallback += self.StartupDirectory
def Cleanup(self):
for stdoutHandler in self.StdoutHandlers:
del stdoutHandler.HandleCallback
del self.InitializeProcessCallback
del self.RenderExecutableCallback
del self.RenderArgumentCallback
del self.StartupDirectoryCallback
def GetCelActionRegistryKey(self):
# Modify registry for frame separation
path = r'Software\CelAction\CelAction2D\User Settings'
_winreg.CreateKey(_winreg.HKEY_CURRENT_USER, path)
regKey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, path, 0,
_winreg.KEY_ALL_ACCESS)
return regKey
def GetSeparatorValue(self, regKey):
useSeparator, _ = _winreg.QueryValueEx(
regKey, 'RenderNameUseSeparator')
separator, _ = _winreg.QueryValueEx(regKey, 'RenderNameSeparator')
return useSeparator, separator
def SetSeparatorValue(self, regKey, useSeparator, separator):
_winreg.SetValueEx(regKey, 'RenderNameUseSeparator',
0, _winreg.REG_DWORD, useSeparator)
_winreg.SetValueEx(regKey, 'RenderNameSeparator',
0, _winreg.REG_SZ, separator)
def InitializeProcess(self):
# Set the plugin specific settings.
self.SingleFramesOnly = False
# Set the process specific settings.
self.StdoutHandling = True
self.PopupHandling = True
# Ignore 'celaction' Pop-up dialog
self.AddPopupIgnorer(".*Rendering.*")
self.AddPopupIgnorer(".*AutoRender.*")
# Ignore 'celaction' Pop-up dialog
self.AddPopupIgnorer(".*Wait.*")
# Ignore 'celaction' Pop-up dialog
self.AddPopupIgnorer(".*Timeline Scrub.*")
celActionRegKey = self.GetCelActionRegistryKey()
self.SetSeparatorValue(celActionRegKey, 1, self.GetConfigEntryWithDefault(
"RenderNameSeparator", ".").strip())
def RenderExecutable(self):
return RepositoryUtils.CheckPathMapping(self.GetConfigEntry("Executable").strip())
def RenderArgument(self):
arguments = RepositoryUtils.CheckPathMapping(
self.GetPluginInfoEntry("Arguments").strip())
arguments = arguments.replace(
"<STARTFRAME>", str(self.GetStartFrame()))
arguments = arguments.replace("<ENDFRAME>", str(self.GetEndFrame()))
arguments = self.ReplacePaddedFrame(
arguments, "<STARTFRAME%([0-9]+)>", self.GetStartFrame())
arguments = self.ReplacePaddedFrame(
arguments, "<ENDFRAME%([0-9]+)>", self.GetEndFrame())
arguments = arguments.replace("<QUOTE>", "\"")
return arguments
def StartupDirectory(self):
return self.GetPluginInfoEntryWithDefault("StartupDirectory", "").strip()
def ReplacePaddedFrame(self, arguments, pattern, frame):
frameRegex = Regex(pattern)
while True:
frameMatch = frameRegex.Match(arguments)
if frameMatch.Success:
paddingSize = int(frameMatch.Groups[1].Value)
if paddingSize > 0:
padding = StringUtils.ToZeroPaddedString(
frame, paddingSize, False)
else:
padding = str(frame)
arguments = arguments.replace(
frameMatch.Groups[0].Value, padding)
else:
break
return arguments

View file

@ -38,7 +38,7 @@ class IntegrateFtrackDescription(pyblish.api.InstancePlugin):
self.log.info("There are any integrated AssetVersions")
return
comment = (instance.context.data.get("comment") or "").strip()
comment = instance.data["comment"]
if not comment:
self.log.info("Comment is not set.")
else:

View file

@ -45,7 +45,7 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin):
host_name = context.data["hostName"]
app_name = context.data["appName"]
app_label = context.data["appLabel"]
comment = (context.data.get("comment") or "").strip()
comment = instance.data["comment"]
if not comment:
self.log.info("Comment is not set.")
else:

View file

@ -1,3 +1,4 @@
import inspect
from abc import ABCMeta
import pyblish.api
@ -132,6 +133,25 @@ class OpenPypePyblishPluginMixin:
)
return attribute_values
@staticmethod
def get_attr_values_from_data_for_plugin(plugin, data):
"""Get attribute values for attribute definitions from data.
Args:
plugin (Union[publish.api.Plugin, Type[publish.api.Plugin]]): The
plugin for which attributes are extracted.
data(dict): Data from instance or context.
"""
if not inspect.isclass(plugin):
plugin = plugin.__class__
return (
data
.get("publish_attributes", {})
.get(plugin.__name__, {})
)
def get_attr_values_from_data(self, data):
"""Get attribute values for attribute definitions from data.
@ -139,11 +159,7 @@ class OpenPypePyblishPluginMixin:
data(dict): Data from instance or context.
"""
return (
data
.get("publish_attributes", {})
.get(self.__class__.__name__, {})
)
return self.get_attr_values_from_data_for_plugin(self.__class__, data)
class OptionalPyblishPluginMixin(OpenPypePyblishPluginMixin):

View file

@ -1,21 +1,27 @@
import collections
import pyblish.api
from openpype.client import (
get_last_version_by_subset_name,
get_assets,
get_subsets,
get_last_versions,
get_representations,
)
from openpype.pipeline import (
legacy_io,
get_representation_path,
)
from openpype.pipeline.load import get_representation_path_with_anatomy
class CollectAudio(pyblish.api.InstancePlugin):
class CollectAudio(pyblish.api.ContextPlugin):
"""Collect asset's last published audio.
The audio subset name searched for is defined in:
project settings > Collect Audio
Note:
The plugin was instance plugin but because of so much queries the
plugin was slowing down whole collection phase a lot thus was
converted to context plugin which requires only 4 queries top.
"""
label = "Collect Asset Audio"
order = pyblish.api.CollectorOrder + 0.1
families = ["review"]
@ -39,67 +45,134 @@ class CollectAudio(pyblish.api.InstancePlugin):
audio_subset_name = "audioMain"
def process(self, instance):
if instance.data.get("audio"):
self.log.info(
"Skipping Audio collecion. It is already collected"
)
def process(self, context):
# Fake filtering by family inside context plugin
filtered_instances = []
for instance in pyblish.api.instances_by_plugin(
context, self.__class__
):
# Skip instances that already have audio filled
if instance.data.get("audio"):
self.log.info(
"Skipping Audio collecion. It is already collected"
)
continue
filtered_instances.append(instance)
# Skip if none of instances remained
if not filtered_instances:
return
# Add audio to instance if exists.
instances_by_asset_name = collections.defaultdict(list)
for instance in filtered_instances:
asset_name = instance.data["asset"]
instances_by_asset_name[asset_name].append(instance)
asset_names = set(instances_by_asset_name.keys())
self.log.info((
"Searching for audio subset '{subset}'"
" in asset '{asset}'"
"Searching for audio subset '{subset}' in assets {assets}"
).format(
subset=self.audio_subset_name,
asset=instance.data["asset"]
assets=", ".join([
'"{}"'.format(asset_name)
for asset_name in asset_names
])
))
repre_doc = self._get_repre_doc(instance)
# Query all required documents
project_name = context.data["projectName"]
anatomy = context.data["anatomy"]
repre_docs_by_asset_names = self.query_representations(
project_name, asset_names)
# Add audio to instance if representation was found
if repre_doc:
instance.data["audio"] = [{
"offset": 0,
"filename": get_representation_path(repre_doc)
}]
self.log.info("Audio Data added to instance ...")
for asset_name, instances in instances_by_asset_name.items():
repre_docs = repre_docs_by_asset_names[asset_name]
if not repre_docs:
continue
def _get_repre_doc(self, instance):
cache = instance.context.data.get("__cache_asset_audio")
if cache is None:
cache = {}
instance.context.data["__cache_asset_audio"] = cache
asset_name = instance.data["asset"]
repre_doc = repre_docs[0]
repre_path = get_representation_path_with_anatomy(
repre_doc, anatomy
)
for instance in instances:
instance.data["audio"] = [{
"offset": 0,
"filename": repre_path
}]
self.log.info("Audio Data added to instance ...")
# first try to get it from cache
if asset_name in cache:
return cache[asset_name]
def query_representations(self, project_name, asset_names):
"""Query representations related to audio subsets for passed assets.
project_name = legacy_io.active_project()
Args:
project_name (str): Project in which we're looking for all
entities.
asset_names (Iterable[str]): Asset names where to look for audio
subsets and their representations.
# Find latest versions document
last_version_doc = get_last_version_by_subset_name(
Returns:
collections.defaultdict[str, List[Dict[Str, Any]]]: Representations
related to audio subsets by asset name.
"""
output = collections.defaultdict(list)
# Query asset documents
asset_docs = get_assets(
project_name,
self.audio_subset_name,
asset_name=asset_name,
fields=["_id"]
asset_names=asset_names,
fields=["_id", "name"]
)
repre_doc = None
if last_version_doc:
# Try to find it's representation (Expected there is only one)
repre_docs = list(get_representations(
project_name, version_ids=[last_version_doc["_id"]]
))
if not repre_docs:
self.log.warning(
"Version document does not contain any representations"
)
else:
repre_doc = repre_docs[0]
asset_id_by_name = {}
for asset_doc in asset_docs:
asset_id_by_name[asset_doc["name"]] = asset_doc["_id"]
asset_ids = set(asset_id_by_name.values())
# update cache
cache[asset_name] = repre_doc
# Query subsets with name define by 'audio_subset_name' attr
# - one or none subsets with the name should be available on an asset
subset_docs = get_subsets(
project_name,
subset_names=[self.audio_subset_name],
asset_ids=asset_ids,
fields=["_id", "parent"]
)
subset_id_by_asset_id = {}
for subset_doc in subset_docs:
asset_id = subset_doc["parent"]
subset_id_by_asset_id[asset_id] = subset_doc["_id"]
return repre_doc
subset_ids = set(subset_id_by_asset_id.values())
if not subset_ids:
return output
# Find all latest versions for the subsets
version_docs_by_subset_id = get_last_versions(
project_name, subset_ids=subset_ids, fields=["_id", "parent"]
)
version_id_by_subset_id = {
subset_id: version_doc["_id"]
for subset_id, version_doc in version_docs_by_subset_id.items()
}
version_ids = set(version_id_by_subset_id.values())
if not version_ids:
return output
# Find representations under latest versions of audio subsets
repre_docs = get_representations(
project_name, version_ids=version_ids
)
repre_docs_by_version_id = collections.defaultdict(list)
for repre_doc in repre_docs:
version_id = repre_doc["parent"]
repre_docs_by_version_id[version_id].append(repre_doc)
if not repre_docs_by_version_id:
return output
for asset_name in asset_names:
asset_id = asset_id_by_name.get(asset_name)
subset_id = subset_id_by_asset_id.get(asset_id)
version_id = version_id_by_subset_id.get(subset_id)
output[asset_name] = repre_docs_by_version_id[version_id]
return output

View file

@ -1,19 +1,123 @@
"""
Requires:
None
Provides:
context -> comment (str)
"""Collect comment and add option to enter comment per instance.
Combination of plugins. One define optional input for instances in Publisher
UI (CollectInstanceCommentDef) and second cares that each instance during
collection has available "comment" key in data (CollectComment).
Plugin 'CollectInstanceCommentDef' define "comment" attribute which won't be
filled with any value if instance does not match families filter or when
plugin is disabled.
Plugin 'CollectComment' makes sure that each instance in context has
available "comment" key in data which can be set to 'str' or 'None' if is not
set.
- In case instance already has filled comment the plugin's logic is skipped
- The comment is always set and value should be always 'str' even if is empty
Why are separated:
- 'CollectInstanceCommentDef' can have specific settings to show comment
attribute only to defined families in publisher UI
- 'CollectComment' will run all the time
Todos:
The comment per instance is not sent via farm.
"""
import pyblish.api
from openpype.lib.attribute_definitions import TextDef
from openpype.pipeline.publish import OpenPypePyblishPluginMixin
class CollectComment(pyblish.api.ContextPlugin):
"""This plug-ins displays the comment dialog box per default"""
class CollectInstanceCommentDef(
pyblish.api.ContextPlugin,
OpenPypePyblishPluginMixin
):
label = "Comment per instance"
targets = ["local"]
# Disable plugin by default
families = []
enabled = False
label = "Collect Comment"
order = pyblish.api.CollectorOrder
def process(self, instance):
pass
@classmethod
def apply_settings(cls, project_setting, _):
plugin_settings = project_setting["global"]["publish"].get(
"collect_comment_per_instance"
)
if not plugin_settings:
return
if plugin_settings.get("enabled") is not None:
cls.enabled = plugin_settings["enabled"]
if plugin_settings.get("families") is not None:
cls.families = plugin_settings["families"]
@classmethod
def get_attribute_defs(cls):
return [
TextDef("comment", label="Comment")
]
class CollectComment(
pyblish.api.ContextPlugin,
OpenPypePyblishPluginMixin
):
"""Collect comment per each instance.
Plugin makes sure each instance to publish has set "comment" in data so any
further plugin can use it directly.
"""
label = "Collect Instance Comment"
order = pyblish.api.CollectorOrder + 0.49
def process(self, context):
comment = (context.data.get("comment") or "").strip()
context.data["comment"] = comment
context_comment = self.cleanup_comment(context.data.get("comment"))
# Set it back
context.data["comment"] = context_comment
for instance in context:
instance_label = str(instance)
# Check if comment is already set
instance_comment = self.cleanup_comment(
instance.data.get("comment"))
# If comment on instance is not set then look for attributes
if not instance_comment:
attr_values = self.get_attr_values_from_data_for_plugin(
CollectInstanceCommentDef, instance.data
)
instance_comment = self.cleanup_comment(
attr_values.get("comment")
)
# Use context comment if instance has all options of comment
# empty
if not instance_comment:
instance_comment = context_comment
instance.data["comment"] = instance_comment
if instance_comment:
msg_end = " has comment set to: \"{}\"".format(
instance_comment)
else:
msg_end = " does not have set comment"
self.log.debug("Instance {} {}".format(instance_label, msg_end))
def cleanup_comment(self, comment):
"""Cleanup comment value.
Args:
comment (Union[str, None]): Comment value from data.
Returns:
str: Cleaned comment which is stripped or empty string if input
was 'None'.
"""
if comment:
return comment.strip()
return ""

View file

@ -468,7 +468,7 @@ class ExtractBurnin(publish.Extractor):
burnin_data.update({
"version": int(version),
"comment": context.data.get("comment") or ""
"comment": instance.data["comment"]
})
intent_label = context.data.get("intent") or ""

View file

@ -73,6 +73,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
"Adding thumbnail representation: {}".format(new_repre)
)
instance.data["representations"].append(new_repre)
instance.data["thumbnailPath"] = dst_filepath
def _create_thumbnail(self, context, thumbnail_source):
if not thumbnail_source:

View file

@ -769,7 +769,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
"time": context.data["time"],
"author": context.data["user"],
"source": source,
"comment": context.data.get("comment"),
"comment": instance.data["comment"],
"machine": context.data.get("machine"),
"fps": instance.data.get("fps", context.data.get("fps"))
}

View file

@ -968,7 +968,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
"time": context.data["time"],
"author": context.data["user"],
"source": source,
"comment": context.data.get("comment"),
"comment": instance.data["comment"],
"machine": context.data.get("machine"),
"fps": context.data.get(
"fps", instance.data.get("fps")

View file

@ -102,8 +102,31 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin):
thumbnail_root
)
def _get_thumbnail_from_instance(self, instance):
# 1. Look for thumbnail in published representations
published_repres = instance.data.get("published_representations")
path = self._get_thumbnail_path_from_published(published_repres)
if path and os.path.exists(path):
return path
if path:
self.log.warning(
"Could not find published thumbnail path {}".format(path)
)
# 2. Look for thumbnail in "not published" representations
thumbnail_path = self._get_thumbnail_path_from_unpublished(instance)
if thumbnail_path and os.path.exists(thumbnail_path):
return thumbnail_path
# 3. Look for thumbnail path on instance in 'thumbnailPath'
thumbnail_path = instance.data.get("thumbnailPath")
if thumbnail_path and os.path.exists(thumbnail_path):
return thumbnail_path
return None
def _prepare_instances(self, context):
context_thumbnail_path = context.get("thumbnailPath")
context_thumbnail_path = context.data.get("thumbnailPath")
valid_context_thumbnail = False
if context_thumbnail_path and os.path.exists(context_thumbnail_path):
valid_context_thumbnail = True
@ -122,8 +145,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin):
continue
# Find thumbnail path on instance
thumbnail_path = self._get_instance_thumbnail_path(
published_repres)
thumbnail_path = self._get_thumbnail_from_instance(instance)
if thumbnail_path:
self.log.debug((
"Found thumbnail path for instance \"{}\"."
@ -157,7 +179,10 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin):
for repre_info in published_representations.values():
return repre_info["representation"]["parent"]
def _get_instance_thumbnail_path(self, published_representations):
def _get_thumbnail_path_from_published(self, published_representations):
if not published_representations:
return None
thumb_repre_doc = None
for repre_info in published_representations.values():
repre_doc = repre_info["representation"]
@ -179,6 +204,38 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin):
return None
return os.path.normpath(path)
def _get_thumbnail_path_from_unpublished(self, instance):
repres = instance.data.get("representations")
if not repres:
return None
thumbnail_repre = next(
(
repre
for repre in repres
if repre["name"] == "thumbnail"
),
None
)
if not thumbnail_repre:
return None
staging_dir = thumbnail_repre.get("stagingDir")
if not staging_dir:
staging_dir = instance.data.get("stagingDir")
filename = thumbnail_repre.get("files")
if not staging_dir or not filename:
return None
if isinstance(filename, (list, tuple, set)):
filename = filename[0]
thumbnail_path = os.path.join(staging_dir, filename)
if os.path.exists(thumbnail_path):
return thumbnail_path
return None
def _integrate_thumbnails(
self,
filtered_instance_items,

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View file

@ -1,13 +1,9 @@
{
"publish": {
"ExtractCelactionDeadline": {
"enabled": true,
"deadline_department": "",
"deadline_priority": 50,
"deadline_pool": "",
"deadline_pool_secondary": "",
"deadline_group": "",
"deadline_chunk_size": 10
"CollectRenderPath": {
"output_extension": "png",
"anatomy_template_key_render_files": "render",
"anatomy_template_key_metadata": "render"
}
}
}

View file

@ -70,6 +70,16 @@
"department": "",
"multiprocess": true
},
"CelactionSubmitDeadline": {
"enabled": true,
"deadline_department": "",
"deadline_priority": 50,
"deadline_pool": "",
"deadline_pool_secondary": "",
"deadline_group": "",
"deadline_chunk_size": 10,
"deadline_job_delay": "00:00:00:00"
},
"ProcessSubmittedJobOnFarm": {
"enabled": true,
"deadline_department": "",

View file

@ -24,6 +24,10 @@
],
"skip_hosts_headless_publish": []
},
"collect_comment_per_instance": {
"enabled": false,
"families": []
},
"ValidateEditorialAssetName": {
"enabled": true,
"optional": false

View file

@ -1268,12 +1268,12 @@
"CELACTION_TEMPLATE": "{OPENPYPE_REPOS_ROOT}/openpype/hosts/celaction/celaction_template_scene.scn"
},
"variants": {
"local": {
"current": {
"enabled": true,
"variant_label": "Local",
"variant_label": "Current",
"use_python_2": false,
"executables": {
"windows": [],
"windows": ["C:/Program Files/CelAction/CelAction2D Studio/CelAction2D.exe"],
"darwin": [],
"linux": []
},

View file

@ -14,45 +14,24 @@
{
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"key": "ExtractCelactionDeadline",
"label": "ExtractCelactionDeadline",
"key": "CollectRenderPath",
"label": "CollectRenderPath",
"is_group": true,
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
"type": "text",
"key": "output_extension",
"label": "Output render file extension"
},
{
"type": "text",
"key": "deadline_department",
"label": "Deadline apartment"
},
{
"type": "number",
"key": "deadline_priority",
"label": "Deadline priority"
"key": "anatomy_template_key_render_files",
"label": "Anatomy template key: render files"
},
{
"type": "text",
"key": "deadline_pool",
"label": "Deadline pool"
},
{
"type": "text",
"key": "deadline_pool_secondary",
"label": "Deadline pool (secondary)"
},
{
"type": "text",
"key": "deadline_group",
"label": "Deadline Group"
},
{
"type": "number",
"key": "deadline_chunk_size",
"label": "Deadline Chunk size"
"key": "anatomy_template_key_metadata",
"label": "Anatomy template key: metadata job file"
}
]
}

View file

@ -387,6 +387,56 @@
}
]
},
{
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"key": "CelactionSubmitDeadline",
"label": "Celaction Submit Deadline",
"is_group": true,
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "text",
"key": "deadline_department",
"label": "Deadline apartment"
},
{
"type": "number",
"key": "deadline_priority",
"label": "Deadline priority"
},
{
"type": "text",
"key": "deadline_pool",
"label": "Deadline pool"
},
{
"type": "text",
"key": "deadline_pool_secondary",
"label": "Deadline pool (secondary)"
},
{
"type": "text",
"key": "deadline_group",
"label": "Deadline Group"
},
{
"type": "number",
"key": "deadline_chunk_size",
"label": "Deadline Chunk size"
},
{
"type": "text",
"key": "deadline_job_delay",
"label": "Delay job (timecode dd:hh:mm:ss)"
}
]
},
{
"type": "dict",
"collapsible": true,

View file

@ -60,6 +60,27 @@
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "collect_comment_per_instance",
"label": "Collect comment per instance",
"checkbox_key": "enabled",
"is_group": true,
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"key": "families",
"label": "Families",
"type": "list",
"object_type": "text"
}
]
},
{
"type": "dict",
"collapsible": true,

View file

@ -28,8 +28,8 @@
"name": "template_host_variant",
"template_data": [
{
"app_variant_label": "Local",
"app_variant": "local"
"app_variant_label": "Current",
"app_variant": "current"
}
]
}

View file

@ -8,7 +8,8 @@ exclude =
docs,
*/vendor,
website,
openpype/vendor
openpype/vendor,
*deadline/repository/custom/plugins
max-complexity = 30