mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge branch 'develop' into bugfix/1586-yn-0299-launcher-my-tasks-view-doesnt-refresh-on-new-assignments
This commit is contained in:
commit
48c5bb2ea7
9 changed files with 522 additions and 147 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -12,6 +13,8 @@ from typing import Optional
|
||||||
|
|
||||||
import xml.etree.ElementTree
|
import xml.etree.ElementTree
|
||||||
|
|
||||||
|
import clique
|
||||||
|
|
||||||
from .execute import run_subprocess
|
from .execute import run_subprocess
|
||||||
from .vendor_bin_utils import (
|
from .vendor_bin_utils import (
|
||||||
get_ffmpeg_tool_args,
|
get_ffmpeg_tool_args,
|
||||||
|
|
@ -634,6 +637,37 @@ def should_convert_for_ffmpeg(src_filepath):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _get_attributes_to_erase(
|
||||||
|
input_info: dict, logger: logging.Logger
|
||||||
|
) -> list[str]:
|
||||||
|
"""FFMPEG does not support some attributes in metadata."""
|
||||||
|
erase_attrs: dict[str, str] = {} # Attr name to reason mapping
|
||||||
|
for attr_name, attr_value in input_info["attribs"].items():
|
||||||
|
if not isinstance(attr_value, str):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Remove attributes that have string value longer than allowed length
|
||||||
|
# for ffmpeg or when contain prohibited symbols
|
||||||
|
if len(attr_value) > MAX_FFMPEG_STRING_LEN:
|
||||||
|
reason = f"has too long value ({len(attr_value)} chars)."
|
||||||
|
erase_attrs[attr_name] = reason
|
||||||
|
continue
|
||||||
|
|
||||||
|
for char in NOT_ALLOWED_FFMPEG_CHARS:
|
||||||
|
if char not in attr_value:
|
||||||
|
continue
|
||||||
|
reason = f"contains unsupported character \"{char}\"."
|
||||||
|
erase_attrs[attr_name] = reason
|
||||||
|
break
|
||||||
|
|
||||||
|
for attr_name, reason in erase_attrs.items():
|
||||||
|
logger.info(
|
||||||
|
f"Removed attribute \"{attr_name}\" from metadata"
|
||||||
|
f" because {reason}."
|
||||||
|
)
|
||||||
|
return list(erase_attrs.keys())
|
||||||
|
|
||||||
|
|
||||||
def convert_input_paths_for_ffmpeg(
|
def convert_input_paths_for_ffmpeg(
|
||||||
input_paths,
|
input_paths,
|
||||||
output_dir,
|
output_dir,
|
||||||
|
|
@ -659,7 +693,7 @@ def convert_input_paths_for_ffmpeg(
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If input filepath has extension not supported by function.
|
ValueError: If input filepath has extension not supported by function.
|
||||||
Currently is supported only ".exr" extension.
|
Currently, only ".exr" extension is supported.
|
||||||
"""
|
"""
|
||||||
if logger is None:
|
if logger is None:
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -684,7 +718,22 @@ def convert_input_paths_for_ffmpeg(
|
||||||
# Collect channels to export
|
# Collect channels to export
|
||||||
input_arg, channels_arg = get_oiio_input_and_channel_args(input_info)
|
input_arg, channels_arg = get_oiio_input_and_channel_args(input_info)
|
||||||
|
|
||||||
for input_path in input_paths:
|
# Find which attributes to strip
|
||||||
|
erase_attributes: list[str] = _get_attributes_to_erase(
|
||||||
|
input_info, logger=logger
|
||||||
|
)
|
||||||
|
|
||||||
|
# clique.PATTERNS["frames"] supports only `.1001.exr` not `_1001.exr` so
|
||||||
|
# we use a customized pattern.
|
||||||
|
pattern = "[_.](?P<index>(?P<padding>0*)\\d+)\\.\\D+\\d?$"
|
||||||
|
input_collections, input_remainder = clique.assemble(
|
||||||
|
input_paths,
|
||||||
|
patterns=[pattern],
|
||||||
|
assume_padded_when_ambiguous=True,
|
||||||
|
)
|
||||||
|
input_items = list(input_collections)
|
||||||
|
input_items.extend(input_remainder)
|
||||||
|
for input_item in input_items:
|
||||||
# Prepare subprocess arguments
|
# Prepare subprocess arguments
|
||||||
oiio_cmd = get_oiio_tool_args(
|
oiio_cmd = get_oiio_tool_args(
|
||||||
"oiiotool",
|
"oiiotool",
|
||||||
|
|
@ -695,8 +744,23 @@ def convert_input_paths_for_ffmpeg(
|
||||||
if compression:
|
if compression:
|
||||||
oiio_cmd.extend(["--compression", compression])
|
oiio_cmd.extend(["--compression", compression])
|
||||||
|
|
||||||
|
# Convert a sequence of files using a single oiiotool command
|
||||||
|
# using its sequence syntax
|
||||||
|
if isinstance(input_item, clique.Collection):
|
||||||
|
frames = input_item.format("{head}#{tail}").replace(" ", "")
|
||||||
|
oiio_cmd.extend([
|
||||||
|
"--framepadding", input_item.padding,
|
||||||
|
"--frames", frames,
|
||||||
|
"--parallel-frames"
|
||||||
|
])
|
||||||
|
input_item: str = input_item.format("{head}#{tail}")
|
||||||
|
elif not isinstance(input_item, str):
|
||||||
|
raise TypeError(
|
||||||
|
f"Input is not a string or Collection: {input_item}"
|
||||||
|
)
|
||||||
|
|
||||||
oiio_cmd.extend([
|
oiio_cmd.extend([
|
||||||
input_arg, input_path,
|
input_arg, input_item,
|
||||||
# Tell oiiotool which channels should be put to top stack
|
# Tell oiiotool which channels should be put to top stack
|
||||||
# (and output)
|
# (and output)
|
||||||
"--ch", channels_arg,
|
"--ch", channels_arg,
|
||||||
|
|
@ -704,38 +768,11 @@ def convert_input_paths_for_ffmpeg(
|
||||||
"--subimage", "0"
|
"--subimage", "0"
|
||||||
])
|
])
|
||||||
|
|
||||||
for attr_name, attr_value in input_info["attribs"].items():
|
for attr_name in erase_attributes:
|
||||||
if not isinstance(attr_value, str):
|
oiio_cmd.extend(["--eraseattrib", attr_name])
|
||||||
continue
|
|
||||||
|
|
||||||
# Remove attributes that have string value longer than allowed
|
|
||||||
# length for ffmpeg or when containing prohibited symbols
|
|
||||||
erase_reason = "Missing reason"
|
|
||||||
erase_attribute = False
|
|
||||||
if len(attr_value) > MAX_FFMPEG_STRING_LEN:
|
|
||||||
erase_reason = "has too long value ({} chars).".format(
|
|
||||||
len(attr_value)
|
|
||||||
)
|
|
||||||
erase_attribute = True
|
|
||||||
|
|
||||||
if not erase_attribute:
|
|
||||||
for char in NOT_ALLOWED_FFMPEG_CHARS:
|
|
||||||
if char in attr_value:
|
|
||||||
erase_attribute = True
|
|
||||||
erase_reason = (
|
|
||||||
"contains unsupported character \"{}\"."
|
|
||||||
).format(char)
|
|
||||||
break
|
|
||||||
|
|
||||||
if erase_attribute:
|
|
||||||
# Set attribute to empty string
|
|
||||||
logger.info((
|
|
||||||
"Removed attribute \"{}\" from metadata because {}."
|
|
||||||
).format(attr_name, erase_reason))
|
|
||||||
oiio_cmd.extend(["--eraseattrib", attr_name])
|
|
||||||
|
|
||||||
# Add last argument - path to output
|
# Add last argument - path to output
|
||||||
base_filename = os.path.basename(input_path)
|
base_filename = os.path.basename(input_item)
|
||||||
output_path = os.path.join(output_dir, base_filename)
|
output_path = os.path.join(output_dir, base_filename)
|
||||||
oiio_cmd.extend([
|
oiio_cmd.extend([
|
||||||
"-o", output_path
|
"-o", output_path
|
||||||
|
|
@ -1136,7 +1173,10 @@ def oiio_color_convert(
|
||||||
target_display=None,
|
target_display=None,
|
||||||
target_view=None,
|
target_view=None,
|
||||||
additional_command_args=None,
|
additional_command_args=None,
|
||||||
logger=None,
|
frames: Optional[str] = None,
|
||||||
|
frame_padding: Optional[int] = None,
|
||||||
|
parallel_frames: bool = False,
|
||||||
|
logger: Optional[logging.Logger] = None,
|
||||||
):
|
):
|
||||||
"""Transcode source file to other with colormanagement.
|
"""Transcode source file to other with colormanagement.
|
||||||
|
|
||||||
|
|
@ -1148,7 +1188,7 @@ def oiio_color_convert(
|
||||||
input_path (str): Path that should be converted. It is expected that
|
input_path (str): Path that should be converted. It is expected that
|
||||||
contains single file or image sequence of same type
|
contains single file or image sequence of same type
|
||||||
(sequence in format 'file.FRAMESTART-FRAMEEND#.ext', see oiio docs,
|
(sequence in format 'file.FRAMESTART-FRAMEEND#.ext', see oiio docs,
|
||||||
eg `big.1-3#.tif`)
|
eg `big.1-3#.tif` or `big.1-3%d.ext` with `frames` argument)
|
||||||
output_path (str): Path to output filename.
|
output_path (str): Path to output filename.
|
||||||
(must follow format of 'input_path', eg. single file or
|
(must follow format of 'input_path', eg. single file or
|
||||||
sequence in 'file.FRAMESTART-FRAMEEND#.ext', `output.1-3#.tif`)
|
sequence in 'file.FRAMESTART-FRAMEEND#.ext', `output.1-3#.tif`)
|
||||||
|
|
@ -1169,6 +1209,13 @@ def oiio_color_convert(
|
||||||
both 'view' and 'display' must be filled (if 'target_colorspace')
|
both 'view' and 'display' must be filled (if 'target_colorspace')
|
||||||
additional_command_args (list): arguments for oiiotool (like binary
|
additional_command_args (list): arguments for oiiotool (like binary
|
||||||
depth for .dpx)
|
depth for .dpx)
|
||||||
|
frames (Optional[str]): Complex frame range to process. This requires
|
||||||
|
input path and output path to use frame token placeholder like
|
||||||
|
`#` or `%d`, e.g. file.#.exr
|
||||||
|
frame_padding (Optional[int]): Frame padding to use for the input and
|
||||||
|
output when using a sequence filepath.
|
||||||
|
parallel_frames (bool): If True, process frames in parallel inside
|
||||||
|
the `oiiotool` process. Only supported in OIIO 2.5.20.0+.
|
||||||
logger (logging.Logger): Logger used for logging.
|
logger (logging.Logger): Logger used for logging.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
|
|
@ -1178,7 +1225,16 @@ def oiio_color_convert(
|
||||||
if logger is None:
|
if logger is None:
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
input_info = get_oiio_info_for_input(input_path, logger=logger)
|
# Get oiioinfo only from first image, otherwise file can't be found
|
||||||
|
first_input_path = input_path
|
||||||
|
if frames:
|
||||||
|
frames: str
|
||||||
|
first_frame = int(re.split("[ x-]", frames, 1)[0])
|
||||||
|
first_frame = str(first_frame).zfill(frame_padding or 0)
|
||||||
|
for token in ["#", "%d"]:
|
||||||
|
first_input_path = first_input_path.replace(token, first_frame)
|
||||||
|
|
||||||
|
input_info = get_oiio_info_for_input(first_input_path, logger=logger)
|
||||||
|
|
||||||
# Collect channels to export
|
# Collect channels to export
|
||||||
input_arg, channels_arg = get_oiio_input_and_channel_args(input_info)
|
input_arg, channels_arg = get_oiio_input_and_channel_args(input_info)
|
||||||
|
|
@ -1191,6 +1247,22 @@ def oiio_color_convert(
|
||||||
"--colorconfig", config_path
|
"--colorconfig", config_path
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if frames:
|
||||||
|
# If `frames` is specified, then process the input and output
|
||||||
|
# as if it's a sequence of frames (must contain `%04d` as frame
|
||||||
|
# token placeholder in filepaths)
|
||||||
|
oiio_cmd.extend([
|
||||||
|
"--frames", frames,
|
||||||
|
])
|
||||||
|
|
||||||
|
if frame_padding:
|
||||||
|
oiio_cmd.extend([
|
||||||
|
"--framepadding", str(frame_padding),
|
||||||
|
])
|
||||||
|
|
||||||
|
if parallel_frames:
|
||||||
|
oiio_cmd.append("--parallel-frames")
|
||||||
|
|
||||||
oiio_cmd.extend([
|
oiio_cmd.extend([
|
||||||
input_arg, input_path,
|
input_arg, input_path,
|
||||||
# Tell oiiotool which channels should be put to top stack
|
# Tell oiiotool which channels should be put to top stack
|
||||||
|
|
@ -1234,17 +1306,11 @@ def oiio_color_convert(
|
||||||
if source_view and source_display:
|
if source_view and source_display:
|
||||||
color_convert_args = None
|
color_convert_args = None
|
||||||
ocio_display_args = None
|
ocio_display_args = None
|
||||||
oiio_cmd.extend([
|
|
||||||
"--ociodisplay:inverse=1:subimages=0",
|
|
||||||
source_display,
|
|
||||||
source_view,
|
|
||||||
])
|
|
||||||
|
|
||||||
if target_colorspace:
|
if target_colorspace:
|
||||||
# This is a two-step conversion process since there's no direct
|
# This is a two-step conversion process since there's no direct
|
||||||
# display/view to colorspace command
|
# display/view to colorspace command
|
||||||
# This could be a config parameter or determined from OCIO config
|
# This could be a config parameter or determined from OCIO config
|
||||||
# Use temporarty role space 'scene_linear'
|
# Use temporary role space 'scene_linear'
|
||||||
color_convert_args = ("scene_linear", target_colorspace)
|
color_convert_args = ("scene_linear", target_colorspace)
|
||||||
elif source_display != target_display or source_view != target_view:
|
elif source_display != target_display or source_view != target_view:
|
||||||
# Complete display/view pair conversion
|
# Complete display/view pair conversion
|
||||||
|
|
@ -1256,6 +1322,15 @@ def oiio_color_convert(
|
||||||
" No color conversion needed."
|
" No color conversion needed."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if color_convert_args or ocio_display_args:
|
||||||
|
# Invert source display/view so that we can go from there to the
|
||||||
|
# target colorspace or display/view
|
||||||
|
oiio_cmd.extend([
|
||||||
|
"--ociodisplay:inverse=1:subimages=0",
|
||||||
|
source_display,
|
||||||
|
source_view,
|
||||||
|
])
|
||||||
|
|
||||||
if color_convert_args:
|
if color_convert_args:
|
||||||
# Use colorconvert for colorspace target
|
# Use colorconvert for colorspace target
|
||||||
oiio_cmd.extend([
|
oiio_cmd.extend([
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,15 @@ class BaseCreator(ABC):
|
||||||
project_settings (dict[str, Any]): Project settings.
|
project_settings (dict[str, Any]): Project settings.
|
||||||
create_context (CreateContext): Context which initialized creator.
|
create_context (CreateContext): Context which initialized creator.
|
||||||
headless (bool): Running in headless mode.
|
headless (bool): Running in headless mode.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# Attribute 'skip_discovery' is used during discovery phase to skip
|
||||||
|
# plugins, which can be used to mark base plugins that should not be
|
||||||
|
# considered as plugins "to use". The discovery logic does NOT use
|
||||||
|
# the attribute value from parent classes. Each base class has to define
|
||||||
|
# the attribute again.
|
||||||
|
skip_discovery = True
|
||||||
|
|
||||||
# Label shown in UI
|
# Label shown in UI
|
||||||
label = None
|
label = None
|
||||||
group_label = None
|
group_label = None
|
||||||
|
|
@ -544,11 +552,6 @@ class BaseCreator(ABC):
|
||||||
if host_name is None:
|
if host_name is None:
|
||||||
host_name = self.create_context.host_name
|
host_name = self.create_context.host_name
|
||||||
|
|
||||||
task_name = task_type = None
|
|
||||||
if task_entity:
|
|
||||||
task_name = task_entity["name"]
|
|
||||||
task_type = task_entity["taskType"]
|
|
||||||
|
|
||||||
dynamic_data = self.get_dynamic_data(
|
dynamic_data = self.get_dynamic_data(
|
||||||
project_name,
|
project_name,
|
||||||
folder_entity,
|
folder_entity,
|
||||||
|
|
@ -564,16 +567,15 @@ class BaseCreator(ABC):
|
||||||
|
|
||||||
return get_product_name(
|
return get_product_name(
|
||||||
project_name,
|
project_name,
|
||||||
task_name,
|
folder_entity=folder_entity,
|
||||||
task_type,
|
task_entity=task_entity,
|
||||||
host_name,
|
product_base_type=self.product_base_type,
|
||||||
self.product_type,
|
product_type=self.product_type,
|
||||||
variant,
|
host_name=host_name,
|
||||||
|
variant=variant,
|
||||||
dynamic_data=dynamic_data,
|
dynamic_data=dynamic_data,
|
||||||
project_settings=self.project_settings,
|
project_settings=self.project_settings,
|
||||||
project_entity=project_entity,
|
project_entity=project_entity,
|
||||||
# until we make product_base_type mandatory
|
|
||||||
product_base_type=self.product_base_type
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_instance_attr_defs(self):
|
def get_instance_attr_defs(self):
|
||||||
|
|
@ -648,7 +650,7 @@ class Creator(BaseCreator):
|
||||||
|
|
||||||
Creation requires prepared product name and instance data.
|
Creation requires prepared product name and instance data.
|
||||||
"""
|
"""
|
||||||
|
skip_discovery = True
|
||||||
# GUI Purposes
|
# GUI Purposes
|
||||||
# - default_variants may not be used if `get_default_variants`
|
# - default_variants may not be used if `get_default_variants`
|
||||||
# is overridden
|
# is overridden
|
||||||
|
|
@ -937,6 +939,8 @@ class Creator(BaseCreator):
|
||||||
|
|
||||||
|
|
||||||
class HiddenCreator(BaseCreator):
|
class HiddenCreator(BaseCreator):
|
||||||
|
skip_discovery = True
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def create(self, instance_data, source_data):
|
def create(self, instance_data, source_data):
|
||||||
pass
|
pass
|
||||||
|
|
@ -947,6 +951,7 @@ class AutoCreator(BaseCreator):
|
||||||
|
|
||||||
Can be used e.g. for `workfile`.
|
Can be used e.g. for `workfile`.
|
||||||
"""
|
"""
|
||||||
|
skip_discovery = True
|
||||||
|
|
||||||
def remove_instances(self, instances):
|
def remove_instances(self, instances):
|
||||||
"""Skip removal."""
|
"""Skip removal."""
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
"""Functions for handling product names."""
|
"""Functions for handling product names."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Optional, Union
|
import warnings
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Any, Optional, Union, overload
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
import ayon_api
|
import ayon_api
|
||||||
|
|
@ -9,12 +11,17 @@ from ayon_core.lib import (
|
||||||
StringTemplate,
|
StringTemplate,
|
||||||
filter_profiles,
|
filter_profiles,
|
||||||
prepare_template_data,
|
prepare_template_data,
|
||||||
|
Logger,
|
||||||
|
is_func_signature_supported,
|
||||||
)
|
)
|
||||||
|
from ayon_core.lib.path_templates import TemplateResult
|
||||||
from ayon_core.settings import get_project_settings
|
from ayon_core.settings import get_project_settings
|
||||||
|
|
||||||
from .constants import DEFAULT_PRODUCT_TEMPLATE
|
from .constants import DEFAULT_PRODUCT_TEMPLATE
|
||||||
from .exceptions import TaskNotSetError, TemplateFillError
|
from .exceptions import TaskNotSetError, TemplateFillError
|
||||||
|
|
||||||
|
log = Logger.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_product_name_template(
|
def get_product_name_template(
|
||||||
project_name: str,
|
project_name: str,
|
||||||
|
|
@ -82,7 +89,7 @@ def get_product_name_template(
|
||||||
return template
|
return template
|
||||||
|
|
||||||
|
|
||||||
def get_product_name(
|
def _get_product_name_old(
|
||||||
project_name: str,
|
project_name: str,
|
||||||
task_name: Optional[str],
|
task_name: Optional[str],
|
||||||
task_type: Optional[str],
|
task_type: Optional[str],
|
||||||
|
|
@ -94,61 +101,16 @@ def get_product_name(
|
||||||
project_settings: Optional[dict[str, Any]] = None,
|
project_settings: Optional[dict[str, Any]] = None,
|
||||||
product_type_filter: Optional[str] = None,
|
product_type_filter: Optional[str] = None,
|
||||||
project_entity: Optional[dict[str, Any]] = None,
|
project_entity: Optional[dict[str, Any]] = None,
|
||||||
product_base_type: Optional[str] = None
|
product_base_type: Optional[str] = None,
|
||||||
):
|
) -> TemplateResult:
|
||||||
"""Calculate product name based on passed context and AYON settings.
|
warnings.warn(
|
||||||
|
"Used deprecated 'task_name' and 'task_type' arguments."
|
||||||
Subst name templates are defined in `project_settings/global/tools/creator
|
" Please use new signature with 'folder_entity' and 'task_entity'.",
|
||||||
/product_name_profiles` where are profiles with host name, product type,
|
DeprecationWarning,
|
||||||
task name and task type filters. If context does not match any profile
|
stacklevel=2
|
||||||
then `DEFAULT_PRODUCT_TEMPLATE` is used as default template.
|
)
|
||||||
|
|
||||||
That's main reason why so many arguments are required to calculate product
|
|
||||||
name.
|
|
||||||
|
|
||||||
Deprecation:
|
|
||||||
The `product_base_type` argument is optional now, but it will be
|
|
||||||
mandatory in future versions. It is recommended to pass it now to
|
|
||||||
avoid issues in the future. If it is not passed, a warning will be
|
|
||||||
raised to inform about this change.
|
|
||||||
|
|
||||||
Todos:
|
|
||||||
Find better filtering options to avoid requirement of
|
|
||||||
argument 'family_filter'.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
project_name (str): Project name.
|
|
||||||
task_name (Optional[str]): Task name.
|
|
||||||
task_type (Optional[str]): Task type.
|
|
||||||
host_name (str): Host name.
|
|
||||||
product_type (str): Product type.
|
|
||||||
variant (str): In most of the cases it is user input during creation.
|
|
||||||
default_template (Optional[str]): Default template if any profile does
|
|
||||||
not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE'
|
|
||||||
is used if is not passed.
|
|
||||||
dynamic_data (Optional[Dict[str, Any]]): Dynamic data specific for
|
|
||||||
a creator which creates instance.
|
|
||||||
project_settings (Optional[Union[Dict[str, Any]]]): Prepared settings
|
|
||||||
for project. Settings are queried if not passed.
|
|
||||||
product_type_filter (Optional[str]): Use different product type for
|
|
||||||
product template filtering. Value of `product_type` is used when
|
|
||||||
not passed.
|
|
||||||
project_entity (Optional[Dict[str, Any]]): Project entity used when
|
|
||||||
task short name is required by template.
|
|
||||||
product_base_type (Optional[str]): Base type of product.
|
|
||||||
This will be mandatory in future versions.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Product name.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TaskNotSetError: If template requires task which is not provided.
|
|
||||||
TemplateFillError: If filled template contains placeholder key which
|
|
||||||
is not collected.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not product_type:
|
if not product_type:
|
||||||
return ""
|
return StringTemplate("").format({})
|
||||||
|
|
||||||
template = get_product_name_template(
|
template = get_product_name_template(
|
||||||
project_name=project_name,
|
project_name=project_name,
|
||||||
|
|
@ -160,19 +122,26 @@ def get_product_name(
|
||||||
project_settings=project_settings,
|
project_settings=project_settings,
|
||||||
product_base_type=product_base_type,
|
product_base_type=product_base_type,
|
||||||
)
|
)
|
||||||
# Simple check of task name existence for template with {task} in
|
|
||||||
# - missing task should be possible only in Standalone publisher
|
template_low = template.lower()
|
||||||
if not task_name and "{task" in template.lower():
|
# Simple check of task name existence for template with {task[name]} in
|
||||||
|
if not task_name and "{task" in template_low:
|
||||||
raise TaskNotSetError()
|
raise TaskNotSetError()
|
||||||
|
|
||||||
task_value = {
|
task_value = {
|
||||||
"name": task_name,
|
"name": task_name,
|
||||||
"type": task_type,
|
"type": task_type,
|
||||||
}
|
}
|
||||||
if "{task}" in template.lower():
|
if "{task}" in template_low:
|
||||||
task_value = task_name
|
task_value = task_name
|
||||||
|
# NOTE this is message for TDs and Admins -> not really for users
|
||||||
|
# TODO validate this in settings and not allow it
|
||||||
|
log.warning(
|
||||||
|
"Found deprecated task key '{task}' in product name template."
|
||||||
|
" Please use '{task[name]}' instead."
|
||||||
|
)
|
||||||
|
|
||||||
elif "{task[short]}" in template.lower():
|
elif "{task[short]}" in template_low:
|
||||||
if project_entity is None:
|
if project_entity is None:
|
||||||
project_entity = ayon_api.get_project(project_name)
|
project_entity = ayon_api.get_project(project_name)
|
||||||
task_types_by_name = {
|
task_types_by_name = {
|
||||||
|
|
@ -217,3 +186,305 @@ def get_product_name(
|
||||||
f" Available values are {fill_pairs}"
|
f" Available values are {fill_pairs}"
|
||||||
)
|
)
|
||||||
raise TemplateFillError(msg) from exp
|
raise TemplateFillError(msg) from exp
|
||||||
|
|
||||||
|
|
||||||
|
def _backwards_compatibility_product_name(func):
|
||||||
|
"""Helper to decide which variant of 'get_product_name' to use.
|
||||||
|
|
||||||
|
The old version expected 'task_name' and 'task_type' arguments. The new
|
||||||
|
version expects 'folder_entity' and 'task_entity' arguments instead.
|
||||||
|
|
||||||
|
The function is also marked with an attribute 'version' so other addons
|
||||||
|
can check if the function is using the new signature or is using
|
||||||
|
the old signature. That should allow addons to adapt to new signature.
|
||||||
|
>>> if getattr(get_product_name, "use_entities", None):
|
||||||
|
>>> # New signature is used
|
||||||
|
>>> path = get_product_name(project_name, folder_entity, ...)
|
||||||
|
>>> else:
|
||||||
|
>>> # Old signature is used
|
||||||
|
>>> path = get_product_name(project_name, taks_name, ...)
|
||||||
|
"""
|
||||||
|
# Add attribute to function to identify it as the new function
|
||||||
|
# so other addons can easily identify it.
|
||||||
|
# >>> geattr(get_product_name, "use_entities", False)
|
||||||
|
setattr(func, "use_entities", True)
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
# ---
|
||||||
|
# Decide which variant of the function is used based on
|
||||||
|
# passed arguments.
|
||||||
|
# ---
|
||||||
|
|
||||||
|
# Entities in key-word arguments mean that the new function is used
|
||||||
|
if "folder_entity" in kwargs or "task_entity" in kwargs:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
# Using more than 7 positional arguments is not allowed
|
||||||
|
# in the new function
|
||||||
|
if len(args) > 7:
|
||||||
|
return _get_product_name_old(*args, **kwargs)
|
||||||
|
|
||||||
|
if len(args) > 1:
|
||||||
|
arg_2 = args[1]
|
||||||
|
# The second argument is a string -> task name
|
||||||
|
if isinstance(arg_2, str):
|
||||||
|
return _get_product_name_old(*args, **kwargs)
|
||||||
|
|
||||||
|
if is_func_signature_supported(func, *args, **kwargs):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
return _get_product_name_old(*args, **kwargs)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_product_name(
|
||||||
|
project_name: str,
|
||||||
|
folder_entity: dict[str, Any],
|
||||||
|
task_entity: Optional[dict[str, Any]],
|
||||||
|
product_base_type: str,
|
||||||
|
product_type: str,
|
||||||
|
host_name: str,
|
||||||
|
variant: str,
|
||||||
|
*,
|
||||||
|
dynamic_data: Optional[dict[str, Any]] = None,
|
||||||
|
project_settings: Optional[dict[str, Any]] = None,
|
||||||
|
project_entity: Optional[dict[str, Any]] = None,
|
||||||
|
default_template: Optional[str] = None,
|
||||||
|
product_base_type_filter: Optional[str] = None,
|
||||||
|
) -> TemplateResult:
|
||||||
|
"""Calculate product name based on passed context and AYON settings.
|
||||||
|
|
||||||
|
Subst name templates are defined in `project_settings/global/tools/creator
|
||||||
|
/product_name_profiles` where are profiles with host name, product type,
|
||||||
|
task name and task type filters. If context does not match any profile
|
||||||
|
then `DEFAULT_PRODUCT_TEMPLATE` is used as default template.
|
||||||
|
|
||||||
|
That's main reason why so many arguments are required to calculate product
|
||||||
|
name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_name (str): Project name.
|
||||||
|
folder_entity (Optional[dict[str, Any]]): Folder entity.
|
||||||
|
task_entity (Optional[dict[str, Any]]): Task entity.
|
||||||
|
host_name (str): Host name.
|
||||||
|
product_base_type (str): Product base type.
|
||||||
|
product_type (str): Product type.
|
||||||
|
variant (str): In most of the cases it is user input during creation.
|
||||||
|
dynamic_data (Optional[dict[str, Any]]): Dynamic data specific for
|
||||||
|
a creator which creates instance.
|
||||||
|
project_settings (Optional[dict[str, Any]]): Prepared settings
|
||||||
|
for project. Settings are queried if not passed.
|
||||||
|
project_entity (Optional[dict[str, Any]]): Project entity used when
|
||||||
|
task short name is required by template.
|
||||||
|
default_template (Optional[str]): Default template if any profile does
|
||||||
|
not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE'
|
||||||
|
is used if is not passed.
|
||||||
|
product_base_type_filter (Optional[str]): Use different product base
|
||||||
|
type for product template filtering. Value of
|
||||||
|
`product_base_type_filter` is used when not passed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TemplateResult: Product name.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TaskNotSetError: If template requires task which is not provided.
|
||||||
|
TemplateFillError: If filled template contains placeholder key which
|
||||||
|
is not collected.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_product_name(
|
||||||
|
project_name,
|
||||||
|
task_name,
|
||||||
|
task_type,
|
||||||
|
host_name,
|
||||||
|
product_type,
|
||||||
|
variant,
|
||||||
|
default_template=None,
|
||||||
|
dynamic_data=None,
|
||||||
|
project_settings=None,
|
||||||
|
product_type_filter=None,
|
||||||
|
project_entity=None,
|
||||||
|
) -> TemplateResult:
|
||||||
|
"""Calculate product name based on passed context and AYON settings.
|
||||||
|
|
||||||
|
Product name templates are defined in `project_settings/global/tools
|
||||||
|
/creator/product_name_profiles` where are profiles with host name,
|
||||||
|
product type, task name and task type filters. If context does not match
|
||||||
|
any profile then `DEFAULT_PRODUCT_TEMPLATE` is used as default template.
|
||||||
|
|
||||||
|
That's main reason why so many arguments are required to calculate product
|
||||||
|
name.
|
||||||
|
|
||||||
|
Deprecated:
|
||||||
|
This function is using deprecated signature that does not support
|
||||||
|
folder entity data to be used.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_name (str): Project name.
|
||||||
|
task_name (Optional[str]): Task name.
|
||||||
|
task_type (Optional[str]): Task type.
|
||||||
|
host_name (str): Host name.
|
||||||
|
product_type (str): Product type.
|
||||||
|
variant (str): In most of the cases it is user input during creation.
|
||||||
|
default_template (Optional[str]): Default template if any profile does
|
||||||
|
not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE'
|
||||||
|
is used if is not passed.
|
||||||
|
dynamic_data (Optional[Dict[str, Any]]): Dynamic data specific for
|
||||||
|
a creator which creates instance.
|
||||||
|
project_settings (Optional[Union[Dict[str, Any]]]): Prepared settings
|
||||||
|
for project. Settings are queried if not passed.
|
||||||
|
product_type_filter (Optional[str]): Use different product type for
|
||||||
|
product template filtering. Value of `product_type` is used when
|
||||||
|
not passed.
|
||||||
|
project_entity (Optional[Dict[str, Any]]): Project entity used when
|
||||||
|
task short name is required by template.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TemplateResult: Product name.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@_backwards_compatibility_product_name
|
||||||
|
def get_product_name(
|
||||||
|
project_name: str,
|
||||||
|
folder_entity: dict[str, Any],
|
||||||
|
task_entity: Optional[dict[str, Any]],
|
||||||
|
product_base_type: str,
|
||||||
|
product_type: str,
|
||||||
|
host_name: str,
|
||||||
|
variant: str,
|
||||||
|
*,
|
||||||
|
dynamic_data: Optional[dict[str, Any]] = None,
|
||||||
|
project_settings: Optional[dict[str, Any]] = None,
|
||||||
|
project_entity: Optional[dict[str, Any]] = None,
|
||||||
|
default_template: Optional[str] = None,
|
||||||
|
product_base_type_filter: Optional[str] = None,
|
||||||
|
) -> TemplateResult:
|
||||||
|
"""Calculate product name based on passed context and AYON settings.
|
||||||
|
|
||||||
|
Product name templates are defined in `project_settings/global/tools
|
||||||
|
/creator/product_name_profiles` where are profiles with host name,
|
||||||
|
product base type, product type, task name and task type filters.
|
||||||
|
|
||||||
|
If context does not match any profile then `DEFAULT_PRODUCT_TEMPLATE`
|
||||||
|
is used as default template.
|
||||||
|
|
||||||
|
That's main reason why so many arguments are required to calculate product
|
||||||
|
name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_name (str): Project name.
|
||||||
|
folder_entity (Optional[dict[str, Any]]): Folder entity.
|
||||||
|
task_entity (Optional[dict[str, Any]]): Task entity.
|
||||||
|
host_name (str): Host name.
|
||||||
|
product_base_type (str): Product base type.
|
||||||
|
product_type (str): Product type.
|
||||||
|
variant (str): In most of the cases it is user input during creation.
|
||||||
|
dynamic_data (Optional[dict[str, Any]]): Dynamic data specific for
|
||||||
|
a creator which creates instance.
|
||||||
|
project_settings (Optional[dict[str, Any]]): Prepared settings
|
||||||
|
for project. Settings are queried if not passed.
|
||||||
|
project_entity (Optional[dict[str, Any]]): Project entity used when
|
||||||
|
task short name is required by template.
|
||||||
|
default_template (Optional[str]): Default template if any profile does
|
||||||
|
not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE'
|
||||||
|
is used if is not passed.
|
||||||
|
product_base_type_filter (Optional[str]): Use different product base
|
||||||
|
type for product template filtering. Value of
|
||||||
|
`product_base_type_filter` is used when not passed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TemplateResult: Product name.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TaskNotSetError: If template requires task which is not provided.
|
||||||
|
TemplateFillError: If filled template contains placeholder key which
|
||||||
|
is not collected.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not product_type:
|
||||||
|
return StringTemplate("").format({})
|
||||||
|
|
||||||
|
task_name = task_type = None
|
||||||
|
if task_entity:
|
||||||
|
task_name = task_entity["name"]
|
||||||
|
task_type = task_entity["taskType"]
|
||||||
|
|
||||||
|
template = get_product_name_template(
|
||||||
|
project_name=project_name,
|
||||||
|
product_base_type=product_base_type_filter or product_base_type,
|
||||||
|
product_type=product_type,
|
||||||
|
task_name=task_name,
|
||||||
|
task_type=task_type,
|
||||||
|
host_name=host_name,
|
||||||
|
default_template=default_template,
|
||||||
|
project_settings=project_settings,
|
||||||
|
)
|
||||||
|
|
||||||
|
template_low = template.lower()
|
||||||
|
# Simple check of task name existence for template with {task[name]} in
|
||||||
|
if not task_name and "{task" in template_low:
|
||||||
|
raise TaskNotSetError()
|
||||||
|
|
||||||
|
task_value = {
|
||||||
|
"name": task_name,
|
||||||
|
"type": task_type,
|
||||||
|
}
|
||||||
|
if "{task}" in template_low:
|
||||||
|
task_value = task_name
|
||||||
|
# NOTE this is message for TDs and Admins -> not really for users
|
||||||
|
# TODO validate this in settings and not allow it
|
||||||
|
log.warning(
|
||||||
|
"Found deprecated task key '{task}' in product name template."
|
||||||
|
" Please use '{task[name]}' instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
elif "{task[short]}" in template_low:
|
||||||
|
if project_entity is None:
|
||||||
|
project_entity = ayon_api.get_project(project_name)
|
||||||
|
task_types_by_name = {
|
||||||
|
task["name"]: task
|
||||||
|
for task in project_entity["taskTypes"]
|
||||||
|
}
|
||||||
|
task_short = task_types_by_name.get(task_type, {}).get("shortName")
|
||||||
|
task_value["short"] = task_short
|
||||||
|
|
||||||
|
fill_pairs = {
|
||||||
|
"variant": variant,
|
||||||
|
# TODO We should stop support 'family' key.
|
||||||
|
"family": product_type,
|
||||||
|
"task": task_value,
|
||||||
|
"product": {
|
||||||
|
"type": product_type,
|
||||||
|
"basetype": product_base_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if folder_entity:
|
||||||
|
fill_pairs["folder"] = {
|
||||||
|
"name": folder_entity["name"],
|
||||||
|
"type": folder_entity["folderType"],
|
||||||
|
}
|
||||||
|
|
||||||
|
if dynamic_data:
|
||||||
|
# Dynamic data may override default values
|
||||||
|
for key, value in dynamic_data.items():
|
||||||
|
fill_pairs[key] = value
|
||||||
|
|
||||||
|
try:
|
||||||
|
return StringTemplate.format_strict_template(
|
||||||
|
template=template,
|
||||||
|
data=prepare_template_data(fill_pairs)
|
||||||
|
)
|
||||||
|
except KeyError as exp:
|
||||||
|
msg = (
|
||||||
|
f"Value for {exp} key is missing in template '{template}'."
|
||||||
|
f" Available values are {fill_pairs}"
|
||||||
|
)
|
||||||
|
raise TemplateFillError(msg)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,13 @@ from .utils import get_representation_path_from_context
|
||||||
class LoaderPlugin(list):
|
class LoaderPlugin(list):
|
||||||
"""Load representation into host application"""
|
"""Load representation into host application"""
|
||||||
|
|
||||||
|
# Attribute 'skip_discovery' is used during discovery phase to skip
|
||||||
|
# plugins, which can be used to mark base plugins that should not be
|
||||||
|
# considered as plugins "to use". The discovery logic does NOT use
|
||||||
|
# the attribute value from parent classes. Each base class has to define
|
||||||
|
# the attribute again.
|
||||||
|
skip_discovery = True
|
||||||
|
|
||||||
product_types: set[str] = set()
|
product_types: set[str] = set()
|
||||||
product_base_types: Optional[set[str]] = None
|
product_base_types: Optional[set[str]] = None
|
||||||
representations = set()
|
representations = set()
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,14 @@ def discover_plugins(
|
||||||
for item in modules:
|
for item in modules:
|
||||||
filepath, module = item
|
filepath, module = item
|
||||||
result.add_module(module)
|
result.add_module(module)
|
||||||
all_plugins.extend(classes_from_module(base_class, module))
|
for cls in classes_from_module(base_class, module):
|
||||||
|
if cls is base_class:
|
||||||
|
continue
|
||||||
|
# Class has defined 'skip_discovery = True'
|
||||||
|
skip_discovery = cls.__dict__.get("skip_discovery")
|
||||||
|
if skip_discovery is True:
|
||||||
|
continue
|
||||||
|
all_plugins.append(cls)
|
||||||
|
|
||||||
if base_class not in ignored_classes:
|
if base_class not in ignored_classes:
|
||||||
ignored_classes.append(base_class)
|
ignored_classes.append(base_class)
|
||||||
|
|
|
||||||
|
|
@ -172,20 +172,33 @@ class ExtractOIIOTranscode(publish.Extractor):
|
||||||
additional_command_args = (output_def["oiiotool_args"]
|
additional_command_args = (output_def["oiiotool_args"]
|
||||||
["additional_command_args"])
|
["additional_command_args"])
|
||||||
|
|
||||||
sequence_files = self._translate_to_sequence(files_to_convert)
|
sequence_files = self._translate_to_sequence(
|
||||||
|
files_to_convert)
|
||||||
self.log.debug("Files to convert: {}".format(sequence_files))
|
self.log.debug("Files to convert: {}".format(sequence_files))
|
||||||
missing_rgba_review_channels = False
|
missing_rgba_review_channels = False
|
||||||
for file_name in sequence_files:
|
for file_name in sequence_files:
|
||||||
if isinstance(file_name, clique.Collection):
|
if isinstance(file_name, clique.Collection):
|
||||||
# Convert to filepath that can be directly converted
|
# Support sequences with holes by supplying
|
||||||
# by oiio like `frame.1001-1025%04d.exr`
|
# dedicated `--frames` argument to `oiiotool`
|
||||||
file_name: str = file_name.format(
|
# Create `frames` string like "1001-1002,1004,1010-1012
|
||||||
"{head}{range}{padding}{tail}"
|
# Create `filename` string like "file.#.exr"
|
||||||
|
frames = file_name.format("{ranges}").replace(" ", "")
|
||||||
|
frame_padding = file_name.padding
|
||||||
|
file_name = file_name.format("{head}#{tail}")
|
||||||
|
parallel_frames = True
|
||||||
|
elif isinstance(file_name, str):
|
||||||
|
# Single file
|
||||||
|
frames = None
|
||||||
|
frame_padding = None
|
||||||
|
parallel_frames = False
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
f"Unsupported file name type: {type(file_name)}."
|
||||||
|
" Expected str or clique.Collection."
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log.debug("Transcoding file: `{}`".format(file_name))
|
self.log.debug("Transcoding file: `{}`".format(file_name))
|
||||||
input_path = os.path.join(original_staging_dir,
|
input_path = os.path.join(original_staging_dir, file_name)
|
||||||
file_name)
|
|
||||||
output_path = self._get_output_file_path(input_path,
|
output_path = self._get_output_file_path(input_path,
|
||||||
new_staging_dir,
|
new_staging_dir,
|
||||||
output_extension)
|
output_extension)
|
||||||
|
|
@ -201,6 +214,9 @@ class ExtractOIIOTranscode(publish.Extractor):
|
||||||
source_display=source_display,
|
source_display=source_display,
|
||||||
source_view=source_view,
|
source_view=source_view,
|
||||||
additional_command_args=additional_command_args,
|
additional_command_args=additional_command_args,
|
||||||
|
frames=frames,
|
||||||
|
frame_padding=frame_padding,
|
||||||
|
parallel_frames=parallel_frames,
|
||||||
logger=self.log
|
logger=self.log
|
||||||
)
|
)
|
||||||
except MissingRGBAChannelsError as exc:
|
except MissingRGBAChannelsError as exc:
|
||||||
|
|
@ -294,16 +310,18 @@ class ExtractOIIOTranscode(publish.Extractor):
|
||||||
new_repre["files"] = renamed_files
|
new_repre["files"] = renamed_files
|
||||||
|
|
||||||
def _translate_to_sequence(self, files_to_convert):
|
def _translate_to_sequence(self, files_to_convert):
|
||||||
"""Returns original list or a clique.Collection of a sequence.
|
"""Returns original individual filepaths or list of clique.Collection.
|
||||||
|
|
||||||
Uses clique to find frame sequence Collection.
|
Uses clique to find frame sequence, and return the collections instead.
|
||||||
If sequence not found, it returns original list.
|
If sequence not detected in input filenames, it returns original list.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
files_to_convert (list): list of file names
|
files_to_convert (list[str]): list of file names
|
||||||
Returns:
|
Returns:
|
||||||
list[str | clique.Collection]: List of filepaths or a list
|
list[str | clique.Collection]: List of
|
||||||
of Collections (usually one, unless there are holes)
|
filepaths ['fileA.exr', 'fileB.exr']
|
||||||
|
or clique.Collection for a sequence.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
pattern = [clique.PATTERNS["frames"]]
|
pattern = [clique.PATTERNS["frames"]]
|
||||||
collections, _ = clique.assemble(
|
collections, _ = clique.assemble(
|
||||||
|
|
@ -314,14 +332,7 @@ class ExtractOIIOTranscode(publish.Extractor):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Too many collections {}".format(collections))
|
"Too many collections {}".format(collections))
|
||||||
|
|
||||||
collection = collections[0]
|
return collections
|
||||||
# TODO: Technically oiiotool supports holes in the sequence as well
|
|
||||||
# using the dedicated --frames argument to specify the frames.
|
|
||||||
# We may want to use that too so conversions of sequences with
|
|
||||||
# holes will perform faster as well.
|
|
||||||
# Separate the collection so that we have no holes/gaps per
|
|
||||||
# collection.
|
|
||||||
return collection.separate()
|
|
||||||
|
|
||||||
return files_to_convert
|
return files_to_convert
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -924,8 +924,12 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
||||||
|
|
||||||
# Include optional data if present in
|
# Include optional data if present in
|
||||||
optionals = [
|
optionals = [
|
||||||
"frameStart", "frameEnd", "step",
|
"frameStart", "frameEnd",
|
||||||
"handleEnd", "handleStart", "sourceHashes"
|
"handleEnd", "handleStart",
|
||||||
|
"step",
|
||||||
|
"resolutionWidth", "resolutionHeight",
|
||||||
|
"pixelAspect",
|
||||||
|
"sourceHashes"
|
||||||
]
|
]
|
||||||
for key in optionals:
|
for key in optionals:
|
||||||
if key in instance.data:
|
if key in instance.data:
|
||||||
|
|
|
||||||
|
|
@ -310,9 +310,6 @@ class CreateWidget(QtWidgets.QWidget):
|
||||||
folder_path = None
|
folder_path = None
|
||||||
if self._context_change_is_enabled():
|
if self._context_change_is_enabled():
|
||||||
folder_path = self._context_widget.get_selected_folder_path()
|
folder_path = self._context_widget.get_selected_folder_path()
|
||||||
|
|
||||||
if folder_path is None:
|
|
||||||
folder_path = self.get_current_folder_path()
|
|
||||||
return folder_path or None
|
return folder_path or None
|
||||||
|
|
||||||
def _get_folder_id(self):
|
def _get_folder_id(self):
|
||||||
|
|
@ -328,9 +325,6 @@ class CreateWidget(QtWidgets.QWidget):
|
||||||
folder_path = self._context_widget.get_selected_folder_path()
|
folder_path = self._context_widget.get_selected_folder_path()
|
||||||
if folder_path:
|
if folder_path:
|
||||||
task_name = self._context_widget.get_selected_task_name()
|
task_name = self._context_widget.get_selected_task_name()
|
||||||
|
|
||||||
if not task_name:
|
|
||||||
task_name = self.get_current_task_name()
|
|
||||||
return task_name
|
return task_name
|
||||||
|
|
||||||
def _set_context_enabled(self, enabled):
|
def _set_context_enabled(self, enabled):
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ ayon_server_version = ">=1.8.4,<2.0.0"
|
||||||
ayon_launcher_version = ">=1.0.2"
|
ayon_launcher_version = ">=1.0.2"
|
||||||
ayon_required_addons = {}
|
ayon_required_addons = {}
|
||||||
ayon_compatible_addons = {
|
ayon_compatible_addons = {
|
||||||
|
"ayon_third_party": ">=1.3.0",
|
||||||
"ayon_ocio": ">=1.2.1",
|
"ayon_ocio": ">=1.2.1",
|
||||||
"applications": ">=1.1.2",
|
"applications": ">=1.1.2",
|
||||||
"harmony": ">0.4.0",
|
"harmony": ">0.4.0",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue