Merge branch 'develop' into enhancement/extract_review_timecode

This commit is contained in:
Roy Nieterau 2024-03-28 19:41:23 +01:00 committed by GitHub
commit 9a95c2d384
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 358 additions and 98 deletions

View file

@ -12,7 +12,7 @@ from ayon_core.pipeline.publish import (
import ayon_core.hosts.blender.api.action
class ValidateMeshNoNegativeScale(pyblish.api.Validator,
class ValidateMeshNoNegativeScale(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Ensure that meshes don't have a negative scale."""

View file

@ -3,11 +3,11 @@ import sys
from pprint import pformat
class CollectCelactionCliKwargs(pyblish.api.Collector):
class CollectCelactionCliKwargs(pyblish.api.ContextPlugin):
""" Collects all keyword arguments passed from the terminal """
label = "Collect Celaction Cli Kwargs"
order = pyblish.api.Collector.order - 0.1
order = pyblish.api.CollectorOrder - 0.1
def process(self, context):
args = list(sys.argv[1:])

View file

@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import (
)
class ValidateColorSets(pyblish.api.Validator,
class ValidateColorSets(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Validate all meshes in the instance have unlocked normals

View file

@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import (
)
class ValidateMeshNgons(pyblish.api.Validator,
class ValidateMeshNgons(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Ensure that meshes don't have ngons

View file

@ -16,7 +16,7 @@ def _as_report_list(values, prefix="- ", suffix="\n"):
return prefix + (suffix + prefix).join(values)
class ValidateMeshNoNegativeScale(pyblish.api.Validator,
class ValidateMeshNoNegativeScale(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Ensure that meshes don't have a negative scale.

View file

@ -16,7 +16,7 @@ def _as_report_list(values, prefix="- ", suffix="\n"):
return prefix + (suffix + prefix).join(values)
class ValidateMeshNonManifold(pyblish.api.Validator,
class ValidateMeshNonManifold(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Ensure that meshes don't have non-manifold edges or vertices

View file

@ -18,7 +18,7 @@ def _as_report_list(values, prefix="- ", suffix="\n"):
return prefix + (suffix + prefix).join(values)
class ValidateMeshNormalsUnlocked(pyblish.api.Validator,
class ValidateMeshNormalsUnlocked(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Validate all meshes in the instance have unlocked normals

View file

@ -16,7 +16,7 @@ def _as_report_list(values, prefix="- ", suffix="\n"):
return prefix + (suffix + prefix).join(values)
class ValidateNoAnimation(pyblish.api.Validator,
class ValidateNoAnimation(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Ensure no keyframes on nodes in the Instance.

View file

@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import (
)
class ValidateShapeRenderStats(pyblish.api.Validator,
class ValidateShapeRenderStats(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Ensure all render stats are set to the default values."""

View file

@ -12,7 +12,7 @@ from ayon_core.pipeline.publish import (
)
class ValidateShapeZero(pyblish.api.Validator,
class ValidateShapeZero(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Shape components may not have any "tweak" values

View file

@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import (
)
class ValidateTransformZero(pyblish.api.Validator,
class ValidateTransformZero(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Transforms can't have any values

View file

@ -9,7 +9,7 @@ from ayon_core.pipeline.publish import (
)
class ValidateUniqueNames(pyblish.api.Validator,
class ValidateUniqueNames(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""transform names should be unique

View file

@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import (
)
class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator,
class ValidateYetiRigInputShapesInInstance(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Validate if all input nodes are part of the instance's hierarchy"""

View file

@ -2,10 +2,10 @@ import nuke
import pyblish.api
class ExtractScriptSave(pyblish.api.Extractor):
class ExtractScriptSave(pyblish.api.InstancePlugin):
"""Save current Nuke workfile script"""
label = 'Script Save'
order = pyblish.api.Extractor.order - 0.1
order = pyblish.api.ExtractorOrder - 0.1
hosts = ['nuke']
def process(self, instance):

View file

@ -25,8 +25,9 @@ from ayon_core.hosts.tvpaint.lib import (
)
class ExtractSequence(pyblish.api.Extractor):
class ExtractSequence(pyblish.api.InstancePlugin):
label = "Extract Sequence"
order = pyblish.api.ExtractorOrder
hosts = ["tvpaint"]
families = ["review", "render"]

View file

@ -11,19 +11,17 @@ class ClockifyStart(LauncherAction):
order = 500
clockify_api = ClockifyAPI()
def is_compatible(self, session):
def is_compatible(self, selection):
"""Return whether the action is compatible with the session"""
if "AYON_TASK_NAME" in session:
return True
return False
return selection.is_task_selected
def process(self, session, **kwargs):
def process(self, selection, **kwargs):
self.clockify_api.set_api()
user_id = self.clockify_api.user_id
workspace_id = self.clockify_api.workspace_id
project_name = session["AYON_PROJECT_NAME"]
folder_path = session["AYON_FOLDER_PATH"]
task_name = session["AYON_TASK_NAME"]
project_name = selection.project_name
folder_path = selection.folder_path
task_name = selection.task_name
description = "/".join([folder_path.lstrip("/"), task_name])
# fetch folder entity

View file

@ -19,15 +19,18 @@ class ClockifySync(LauncherAction):
order = 500
clockify_api = ClockifyAPI()
def is_compatible(self, session):
def is_compatible(self, selection):
"""Check if there's some projects to sync"""
if selection.is_project_selected:
return True
try:
next(ayon_api.get_projects())
return True
except StopIteration:
return False
def process(self, session, **kwargs):
def process(self, selection, **kwargs):
self.clockify_api.set_api()
workspace_id = self.clockify_api.workspace_id
user_id = self.clockify_api.user_id
@ -37,10 +40,9 @@ class ClockifySync(LauncherAction):
raise ClockifyPermissionsCheckFailed(
"Current CLockify user is missing permissions for this action!"
)
project_name = session.get("AYON_PROJECT_NAME") or ""
if project_name.strip():
projects_to_sync = [ayon_api.get_project(project_name)]
if selection.is_project_selected:
projects_to_sync = [selection.project_entity]
else:
projects_to_sync = ayon_api.get_projects()

View file

@ -1,4 +1,8 @@
import logging
import warnings
import ayon_api
from ayon_core.pipeline.plugin_discover import (
discover,
register_plugin,
@ -10,6 +14,288 @@ from ayon_core.pipeline.plugin_discover import (
from .load.utils import get_representation_path_from_context
class LauncherActionSelection:
"""Object helper to pass selection to actions.
Object support backwards compatibility for 'session' from OpenPype where
environment variable keys were used to define selection.
Args:
project_name (str): Selected project name.
folder_id (str): Selected folder id.
task_id (str): Selected task id.
folder_path (Optional[str]): Selected folder path.
task_name (Optional[str]): Selected task name.
project_entity (Optional[dict[str, Any]]): Project entity.
folder_entity (Optional[dict[str, Any]]): Folder entity.
task_entity (Optional[dict[str, Any]]): Task entity.
"""
def __init__(
self,
project_name,
folder_id,
task_id,
folder_path=None,
task_name=None,
project_entity=None,
folder_entity=None,
task_entity=None
):
self._project_name = project_name
self._folder_id = folder_id
self._task_id = task_id
self._folder_path = folder_path
self._task_name = task_name
self._project_entity = project_entity
self._folder_entity = folder_entity
self._task_entity = task_entity
def __getitem__(self, key):
warnings.warn(
(
"Using deprecated access to selection data. Please use"
" attributes and methods"
" defined by 'LauncherActionSelection'."
),
category=DeprecationWarning
)
if key in {"AYON_PROJECT_NAME", "AVALON_PROJECT"}:
return self.project_name
if key in {"AYON_FOLDER_PATH", "AVALON_ASSET"}:
return self.folder_path
if key in {"AYON_TASK_NAME", "AVALON_TASK"}:
return self.task_name
raise KeyError(f"Key: {key} not found")
def __iter__(self):
for key in self.keys():
yield key
def __contains__(self, key):
warnings.warn(
(
"Using deprecated access to selection data. Please use"
" attributes and methods"
" defined by 'LauncherActionSelection'."
),
category=DeprecationWarning
)
# Fake missing keys check for backwards compatibility
if key in {
"AYON_PROJECT_NAME",
"AVALON_PROJECT",
}:
return self._project_name is not None
if key in {
"AYON_FOLDER_PATH",
"AVALON_ASSET",
}:
return self._folder_id is not None
if key in {
"AYON_TASK_NAME",
"AVALON_TASK",
}:
return self._task_id is not None
return False
def get(self, key, default=None):
"""
Deprecated:
Added for backwards compatibility with older actions.
"""
warnings.warn(
(
"Using deprecated access to selection data. Please use"
" attributes and methods"
" defined by 'LauncherActionSelection'."
),
category=DeprecationWarning
)
try:
return self[key]
except KeyError:
return default
def items(self):
"""
Deprecated:
Added for backwards compatibility with older actions.
"""
for key, value in (
("AYON_PROJECT_NAME", self.project_name),
("AYON_FOLDER_PATH", self.folder_path),
("AYON_TASK_NAME", self.task_name),
):
if value is not None:
yield (key, value)
def keys(self):
"""
Deprecated:
Added for backwards compatibility with older actions.
"""
for key, _ in self.items():
yield key
def values(self):
"""
Deprecated:
Added for backwards compatibility with older actions.
"""
for _, value in self.items():
yield value
def get_project_name(self):
"""Selected project name.
Returns:
Union[str, None]: Selected project name.
"""
return self._project_name
def get_folder_id(self):
"""Selected folder id.
Returns:
Union[str, None]: Selected folder id.
"""
return self._folder_id
def get_folder_path(self):
"""Selected folder path.
Returns:
Union[str, None]: Selected folder path.
"""
if self._folder_id is None:
return None
if self._folder_path is None:
self._folder_path = self.folder_entity["path"]
return self._folder_path
def get_task_id(self):
"""Selected task id.
Returns:
Union[str, None]: Selected task id.
"""
return self._task_id
def get_task_name(self):
"""Selected task name.
Returns:
Union[str, None]: Selected task name.
"""
if self._task_id is None:
return None
if self._task_name is None:
self._task_name = self.task_entity["name"]
return self._task_name
def get_project_entity(self):
"""Project entity for the selection.
Returns:
Union[dict[str, Any], None]: Project entity.
"""
if self._project_name is None:
return None
if self._project_entity is None:
self._project_entity = ayon_api.get_project(self._project_name)
return self._project_entity
def get_folder_entity(self):
"""Folder entity for the selection.
Returns:
Union[dict[str, Any], None]: Folder entity.
"""
if self._project_name is None or self._folder_id is None:
return None
if self._folder_entity is None:
self._folder_entity = ayon_api.get_folder_by_id(
self._project_name, self._folder_id
)
return self._folder_entity
def get_task_entity(self):
"""Task entity for the selection.
Returns:
Union[dict[str, Any], None]: Task entity.
"""
if (
self._project_name is None
or self._task_id is None
):
return None
if self._task_entity is None:
self._task_entity = ayon_api.get_task_by_id(
self._project_name, self._task_id
)
return self._task_entity
@property
def is_project_selected(self):
"""Return whether a project is selected.
Returns:
bool: Whether a project is selected.
"""
return self._project_name is not None
@property
def is_folder_selected(self):
"""Return whether a folder is selected.
Returns:
bool: Whether a folder is selected.
"""
return self._folder_id is not None
@property
def is_task_selected(self):
"""Return whether a task is selected.
Returns:
bool: Whether a task is selected.
"""
return self._task_id is not None
project_name = property(get_project_name)
folder_id = property(get_folder_id)
task_id = property(get_task_id)
folder_path = property(get_folder_path)
task_name = property(get_task_name)
project_entity = property(get_project_entity)
folder_entity = property(get_folder_entity)
task_entity = property(get_task_entity)
class LauncherAction(object):
"""A custom action available"""
name = None
@ -21,17 +307,23 @@ class LauncherAction(object):
log = logging.getLogger("LauncherAction")
log.propagate = True
def is_compatible(self, session):
def is_compatible(self, selection):
"""Return whether the class is compatible with the Session.
Args:
session (dict[str, Union[str, None]]): Session data with
AYON_PROJECT_NAME, AYON_FOLDER_PATH and AYON_TASK_NAME.
"""
selection (LauncherActionSelection): Data with selection.
"""
return True
def process(self, session, **kwargs):
def process(self, selection, **kwargs):
"""Process the action.
Args:
selection (LauncherActionSelection): Data with selection.
**kwargs: Additional arguments.
"""
pass

View file

@ -18,18 +18,14 @@ class OpenTaskPath(LauncherAction):
icon = "folder-open"
order = 500
def is_compatible(self, session):
def is_compatible(self, selection):
"""Return whether the action is compatible with the session"""
return bool(session.get("AYON_FOLDER_PATH"))
return selection.is_folder_selected
def process(self, session, **kwargs):
def process(self, selection, **kwargs):
from qtpy import QtCore, QtWidgets
project_name = session["AYON_PROJECT_NAME"]
folder_path = session["AYON_FOLDER_PATH"]
task_name = session.get("AYON_TASK_NAME", None)
path = self._get_workdir(project_name, folder_path, task_name)
path = self._get_workdir(selection)
if not path:
return
@ -60,16 +56,17 @@ class OpenTaskPath(LauncherAction):
path = path.split(field, 1)[0]
return path
def _get_workdir(self, project_name, folder_path, task_name):
project_entity = ayon_api.get_project(project_name)
folder_entity = ayon_api.get_folder_by_path(project_name, folder_path)
task_entity = ayon_api.get_task_by_name(
project_name, folder_entity["id"], task_name
def _get_workdir(self, selection):
data = get_template_data(
selection.project_entity,
selection.folder_entity,
selection.task_entity
)
data = get_template_data(project_entity, folder_entity, task_entity)
anatomy = Anatomy(project_name)
anatomy = Anatomy(
selection.project_name,
project_entity=selection.project_entity
)
workdir = anatomy.get_template_item(
"work", "default", "folder"
).format(data)

View file

@ -10,7 +10,7 @@ Scene contains one or more outdated loaded containers, eg. versions loaded into
### How to repair?
Use 'Scene Inventory' and update all highlighted old container to latest OR
refresh Publish and switch 'Validate Containers' toggle on 'Options' tab.
refresh Publish and switch 'Validate Containers' toggle on 'Context' tab.
WARNING: Skipping this validator will result in publishing (and probably rendering) old version of loaded assets.
</description>

View file

@ -5,6 +5,7 @@ from ayon_core.lib import Logger, AYONSettingsRegistry
from ayon_core.pipeline.actions import (
discover_launcher_actions,
LauncherAction,
LauncherActionSelection,
)
from ayon_core.pipeline.workfile import should_use_last_workfile_on_launch
@ -69,11 +70,6 @@ class ApplicationAction(LauncherAction):
project_entities = {}
_log = None
required_session_keys = (
"AYON_PROJECT_NAME",
"AYON_FOLDER_PATH",
"AYON_TASK_NAME"
)
@property
def log(self):
@ -81,18 +77,16 @@ class ApplicationAction(LauncherAction):
self._log = Logger.get_logger(self.__class__.__name__)
return self._log
def is_compatible(self, session):
for key in self.required_session_keys:
if not session.get(key):
return False
def is_compatible(self, selection):
if not selection.is_task_selected:
return False
project_name = session["AYON_PROJECT_NAME"]
project_entity = self.project_entities[project_name]
project_entity = self.project_entities[selection.project_name]
apps = project_entity["attrib"].get("applications")
if not apps or self.application.full_name not in apps:
return False
project_settings = self.project_settings[project_name]
project_settings = self.project_settings[selection.project_name]
only_available = project_settings["applications"]["only_available"]
if only_available and not self.application.find_executable():
return False
@ -112,7 +106,7 @@ class ApplicationAction(LauncherAction):
dialog.setDetailedText(details)
dialog.exec_()
def process(self, session, **kwargs):
def process(self, selection, **kwargs):
"""Process the full Application action"""
from ayon_core.lib import (
@ -120,14 +114,11 @@ class ApplicationAction(LauncherAction):
ApplicationLaunchFailed,
)
project_name = session["AYON_PROJECT_NAME"]
folder_path = session["AYON_FOLDER_PATH"]
task_name = session["AYON_TASK_NAME"]
try:
self.application.launch(
project_name=project_name,
folder_path=folder_path,
task_name=task_name,
project_name=selection.project_name,
folder_path=selection.folder_path,
task_name=selection.task_name,
**self.data
)
@ -335,11 +326,11 @@ class ActionsModel:
"""
not_open_workfile_actions = self._get_no_last_workfile_for_context(
project_name, folder_id, task_id)
session = self._prepare_session(project_name, folder_id, task_id)
selection = self._prepare_selection(project_name, folder_id, task_id)
output = []
action_items = self._get_action_items(project_name)
for identifier, action in self._get_action_objects().items():
if not action.is_compatible(session):
if not action.is_compatible(selection):
continue
action_item = action_items[identifier]
@ -374,7 +365,7 @@ class ActionsModel:
)
def trigger_action(self, project_name, folder_id, task_id, identifier):
session = self._prepare_session(project_name, folder_id, task_id)
selection = self._prepare_selection(project_name, folder_id, task_id)
failed = False
error_message = None
action_label = identifier
@ -403,7 +394,7 @@ class ActionsModel:
)
action.data["start_last_workfile"] = start_last_workfile
action.process(session)
action.process(selection)
except Exception as exc:
self.log.warning("Action trigger failed.", exc_info=True)
failed = True
@ -440,29 +431,8 @@ class ActionsModel:
.get(task_id, {})
)
def _prepare_session(self, project_name, folder_id, task_id):
folder_path = None
if folder_id:
folder = self._controller.get_folder_entity(
project_name, folder_id)
if folder:
folder_path = folder["path"]
task_name = None
if task_id:
task = self._controller.get_task_entity(project_name, task_id)
if task:
task_name = task["name"]
return {
"AYON_PROJECT_NAME": project_name,
"AYON_FOLDER_PATH": folder_path,
"AYON_TASK_NAME": task_name,
# Deprecated - kept for backwards compatibility
"AVALON_PROJECT": project_name,
"AVALON_ASSET": folder_path,
"AVALON_TASK": task_name,
}
def _prepare_selection(self, project_name, folder_id, task_id):
return LauncherActionSelection(project_name, folder_id, task_id)
def _get_discovered_action_classes(self):
if self._discovered_actions is None: