Merge branch 'develop' into enhancement/AY-1228_Load-plugins-update

This commit is contained in:
Jakub Trllo 2024-03-18 17:08:25 +01:00
commit 33f6db2b3e
8 changed files with 245 additions and 95 deletions

View file

@ -103,19 +103,18 @@ def extractenvironments(output_json_path, project, asset, task, app, envgroup):
@main_cli.command()
@click.argument("paths", nargs=-1)
@click.option("-t", "--targets", help="Targets module", default=None,
@click.argument("path", required=True)
@click.option("-t", "--targets", help="Targets", default=None,
multiple=True)
@click.option("-g", "--gui", is_flag=True,
help="Show Publish UI", default=False)
def publish(paths, targets, gui):
def publish(path, targets, gui):
"""Start CLI publishing.
Publish collects json from paths provided as an argument.
More than one path is allowed.
Publish collects json from path provided as an argument.
S
"""
Commands.publish(list(paths), targets, gui)
Commands.publish(path, targets, gui)
@main_cli.command(context_settings={"ignore_unknown_options": True})

View file

@ -3,6 +3,7 @@
import os
import sys
import json
import warnings
class Commands:
@ -41,21 +42,21 @@ class Commands:
return click_func
@staticmethod
def publish(paths, targets=None, gui=False):
def publish(path: str, targets: list=None, gui:bool=False) -> None:
"""Start headless publishing.
Publish use json from passed paths argument.
Publish use json from passed path argument.
Args:
paths (list): Paths to jsons.
targets (string): What module should be targeted
(to choose validator for example)
path (str): Path to JSON.
targets (list of str): List of pyblish targets.
gui (bool): Show publish UI.
Raises:
RuntimeError: When there is no path to process.
"""
RuntimeError: When executed with list of JSON paths.
"""
from ayon_core.lib import Logger
from ayon_core.lib.applications import (
get_app_environments_for_context,
@ -73,6 +74,9 @@ class Commands:
import pyblish.api
import pyblish.util
if not isinstance(path, str):
raise RuntimeError("Path to JSON must be a string.")
# Fix older jobs
for src_key, dst_key in (
("AVALON_PROJECT", "AYON_PROJECT_NAME"),
@ -95,11 +99,8 @@ class Commands:
publish_paths = manager.collect_plugin_paths()["publish"]
for path in publish_paths:
pyblish.api.register_plugin_path(path)
if not any(paths):
raise RuntimeError("No publish paths specified")
for plugin_path in publish_paths:
pyblish.api.register_plugin_path(plugin_path)
app_full_name = os.getenv("AYON_APP_NAME")
if app_full_name:
@ -122,7 +123,7 @@ class Commands:
else:
pyblish.api.register_target("farm")
os.environ["AYON_PUBLISH_DATA"] = os.pathsep.join(paths)
os.environ["AYON_PUBLISH_DATA"] = path
os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib
log.info("Running publish ...")

View file

@ -1,43 +0,0 @@
import os
import pyblish.api
from pymxs import runtime as rt
from ayon_core.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
PublishValidationError,
OptionalPyblishPluginMixin
)
from ayon_core.hosts.max.api.lib_rendersettings import RenderSettings
class ValidateDeadlinePublish(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Validates Render File Directory is
not the same in every submission
"""
order = ValidateContentsOrder
families = ["maxrender"]
hosts = ["max"]
label = "Render Output for Deadline"
optional = True
actions = [RepairAction]
def process(self, instance):
if not self.is_active(instance.data):
return
file = rt.maxFileName
filename, ext = os.path.splitext(file)
if filename not in rt.rendOutputFilename:
raise PublishValidationError(
"Render output folder "
"doesn't match the max scene name! "
"Use Repair action to "
"fix the folder file path.."
)
@classmethod
def repair(cls, instance):
container = instance.data.get("instance_node")
RenderSettings().render_output(container)
cls.log.debug("Reset the render output folder...")

View file

@ -0,0 +1,185 @@
import os
import pyblish.api
from pymxs import runtime as rt
from ayon_core.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
PublishValidationError,
OptionalPyblishPluginMixin
)
from ayon_core.hosts.max.api.lib_rendersettings import RenderSettings
class ValidateRenderPasses(OptionalPyblishPluginMixin,
pyblish.api.InstancePlugin):
"""Validates Render Passes before farm submission
"""
order = ValidateContentsOrder
families = ["maxrender"]
hosts = ["max"]
label = "Validate Render Passes"
actions = [RepairAction]
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
bullet_point_invalid_statement = "\n".join(
f"- {err_type}: {filepath}" for err_type, filepath
in invalid
)
report = (
"Invalid render passes found.\n\n"
f"{bullet_point_invalid_statement}\n\n"
"You can use repair action to fix the invalid filepath."
)
raise PublishValidationError(
report, title="Invalid Render Passes")
@classmethod
def get_invalid(cls, instance):
"""Function to get invalid beauty render outputs and
render elements.
1. Check Render Output Folder matches the name of
the current Max Scene, e.g.
The name of the current Max scene:
John_Doe.max
The expected render output directory:
{root[work]}/{project[name]}/{hierarchy}/{asset}/
work/{task[name]}/render/3dsmax/John_Doe/
2. Check image extension(s) of the render output(s)
matches the image format in OP/AYON setting, e.g.
The current image format in settings: png
The expected render outputs: John_Doe.png
3. Check filename of render element ends with the name of
render element from the 3dsMax Render Element Manager.
e.g. The name of render element: RsCryptomatte
The expected filename: {InstanceName}_RsCryptomatte.png
Args:
instance (pyblish.api.Instance): instance
workfile_name (str): filename of the Max scene
Returns:
list: list of invalid filename which doesn't match
with the project name
"""
invalid = []
file = rt.maxFileName
workfile_name, ext = os.path.splitext(file)
if workfile_name not in rt.rendOutputFilename:
cls.log.error(
"Render output folder must include"
f" the max scene name {workfile_name} "
)
invalid_folder_name = os.path.dirname(
rt.rendOutputFilename).replace(
"\\", "/").split("/")[-1]
invalid.append(("Invalid Render Output Folder",
invalid_folder_name))
beauty_fname = os.path.basename(rt.rendOutputFilename)
beauty_name, ext = os.path.splitext(beauty_fname)
invalid_filenames = cls.get_invalid_filenames(
instance, beauty_name)
invalid.extend(invalid_filenames)
invalid_image_format = cls.get_invalid_image_format(
instance, ext.lstrip("."))
invalid.extend(invalid_image_format)
renderer = instance.data["renderer"]
if renderer in [
"ART_Renderer",
"Redshift_Renderer",
"V_Ray_6_Hotfix_3",
"V_Ray_GPU_6_Hotfix_3",
"Default_Scanline_Renderer",
"Quicksilver_Hardware_Renderer",
]:
render_elem = rt.maxOps.GetCurRenderElementMgr()
render_elem_num = render_elem.NumRenderElements()
for i in range(render_elem_num):
renderlayer_name = render_elem.GetRenderElement(i)
renderpass = str(renderlayer_name).rsplit(":", 1)[-1]
rend_file = render_elem.GetRenderElementFilename(i)
if not rend_file:
continue
rend_fname, ext = os.path.splitext(
os.path.basename(rend_file))
invalid_filenames = cls.get_invalid_filenames(
instance, rend_fname, renderpass=renderpass)
invalid.extend(invalid_filenames)
invalid_image_format = cls.get_invalid_image_format(
instance, ext)
invalid.extend(invalid_image_format)
elif renderer == "Arnold":
cls.log.debug(
"Renderpass validation does not support Arnold yet,"
" validation skipped...")
else:
cls.log.debug(
"Skipping render element validation "
f"for renderer: {renderer}")
return invalid
@classmethod
def get_invalid_filenames(cls, instance, file_name, renderpass=None):
"""Function to get invalid filenames from render outputs.
Args:
instance (pyblish.api.Instance): instance
file_name (str): name of the file
renderpass (str, optional): name of the renderpass.
Defaults to None.
Returns:
list: invalid filenames
"""
invalid = []
if instance.name not in file_name:
cls.log.error("The renderpass filename should contain the instance name.")
invalid.append((f"Invalid instance name",
file_name))
if renderpass is not None:
if not file_name.rstrip(".").endswith(renderpass):
cls.log.error(
f"Filename for {renderpass} should "
f"end with {renderpass}: {file_name}"
)
invalid.append((f"Invalid {renderpass}",
os.path.basename(file_name)))
return invalid
@classmethod
def get_invalid_image_format(cls, instance, ext):
"""Function to check if the image format of the render outputs
aligns with that in the setting.
Args:
instance (pyblish.api.Instance): instance
ext (str): image extension
Returns:
list: list of files with invalid image format
"""
invalid = []
settings = instance.context.data["project_settings"].get("max")
image_format = settings["RenderSettings"]["image_format"]
ext = ext.lstrip(".")
if ext != image_format:
msg = (
f"Invalid image format {ext} for render outputs.\n"
f"Should be: {image_format}")
cls.log.error(msg)
invalid.append((msg, ext))
return invalid
@classmethod
def repair(cls, instance):
container = instance.data.get("instance_node")
# TODO: need to rename the function of render_output
RenderSettings().render_output(container)
cls.log.debug("Finished repairing the render output "
"folder and filenames.")

View file

@ -608,7 +608,7 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase):
return get_product_name(
project_name,
task_name,
task_type
task_type,
host_name,
self.layer_instance_prefix or self.product_type,
variant,

View file

@ -330,19 +330,25 @@ def get_timeline_item(media_pool_item: object,
Returns:
object: resolve.TimelineItem
"""
_clip_property = media_pool_item.GetClipProperty
clip_name = _clip_property("File Name")
clip_name = media_pool_item.GetClipProperty("File Name")
output_timeline_item = None
timeline = timeline or get_current_timeline()
with maintain_current_timeline(timeline):
# search the timeline for the added clip
for _ti_data in get_current_timeline_items():
_ti_clip = _ti_data["clip"]["item"]
_ti_clip_property = _ti_clip.GetMediaPoolItem().GetClipProperty
if clip_name in _ti_clip_property("File Name"):
output_timeline_item = _ti_clip
for ti_data in get_current_timeline_items():
ti_clip_item = ti_data["clip"]["item"]
ti_media_pool_item = ti_clip_item.GetMediaPoolItem()
# Skip items that do not have a media pool item, like for example
# an "Adjustment Clip" or a "Fusion Composition" from the effects
# toolbox
if not ti_media_pool_item:
continue
if clip_name in ti_media_pool_item.GetClipProperty("File Name"):
output_timeline_item = ti_clip_item
return output_timeline_item

View file

@ -36,18 +36,18 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin):
def _load_json(self, path):
path = path.strip('\"')
assert os.path.isfile(path), (
"Path to json file doesn't exist. \"{}\"".format(path)
)
if not os.path.isfile(path):
raise FileNotFoundError(
f"Path to json file doesn't exist. \"{path}\"")
data = None
with open(path, "r") as json_file:
try:
data = json.load(json_file)
except Exception as exc:
self.log.error(
"Error loading json: "
"{} - Exception: {}".format(path, exc)
)
"Error loading json: %s - Exception: %s", path, exc)
return data
def _fill_staging_dir(self, data_object, anatomy):
@ -73,30 +73,23 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin):
data_err = "invalid json file - missing data"
required = ["user", "comment",
"job", "instances", "version"]
assert all(elem in data.keys() for elem in required), data_err
if any(elem not in data for elem in required):
raise ValueError(data_err)
if "folderPath" not in data and "asset" not in data:
raise AssertionError(data_err)
raise ValueError(data_err)
if "folderPath" not in data:
data["folderPath"] = data.pop("asset")
# set context by first json file
ctx = self._context.data
ctx["folderPath"] = ctx.get("folderPath") or data.get("folderPath")
ctx["intent"] = ctx.get("intent") or data.get("intent")
ctx["comment"] = ctx.get("comment") or data.get("comment")
ctx["user"] = ctx.get("user") or data.get("user")
ctx["version"] = ctx.get("version") or data.get("version")
# basic sanity check to see if we are working in same context
# if some other json file has different context, bail out.
ctx_err = "inconsistent contexts in json files - %s"
assert ctx.get("folderPath") == data.get("folderPath"), ctx_err % "folderPath"
assert ctx.get("intent") == data.get("intent"), ctx_err % "intent"
assert ctx.get("comment") == data.get("comment"), ctx_err % "comment"
assert ctx.get("user") == data.get("user"), ctx_err % "user"
assert ctx.get("version") == data.get("version"), ctx_err % "version"
# ftrack credentials are passed as environment variables by Deadline
# to publish job, but Muster doesn't pass them.
if data.get("ftrack") and not os.environ.get("FTRACK_API_USER"):
ftrack = data.get("ftrack")
os.environ["FTRACK_API_USER"] = ftrack["FTRACK_API_USER"]
os.environ["FTRACK_API_KEY"] = ftrack["FTRACK_API_KEY"]
os.environ["FTRACK_SERVER"] = ftrack["FTRACK_SERVER"]
# now we can just add instances from json file and we are done
any_staging_dir_persistent = False

View file

@ -116,6 +116,10 @@ class PublishersModel(BaseSettingsModel):
default_factory=ValidateModelNameModel,
title="Validate Model Name"
)
ValidateRenderPasses: BasicValidateModel = SettingsField(
default_factory=BasicValidateModel,
title="Validate Render Passes"
)
ExtractModelObj: BasicValidateModel = SettingsField(
default_factory=BasicValidateModel,
title="Extract OBJ",
@ -185,6 +189,11 @@ DEFAULT_PUBLISH_SETTINGS = {
"optional": True,
"active": False,
},
"ValidateRenderPasses": {
"enabled": True,
"optional": False,
"active": True
},
"ExtractModelObj": {
"enabled": True,
"optional": True,